From add9caf0f2fda8dc4b63d0a246c43df678458186 Mon Sep 17 00:00:00 2001 From: Irwan Cahyono Date: Wed, 8 Oct 2025 11:39:49 +0700 Subject: [PATCH] mission --- freekake_api/entertainment/serializers.py | 2 +- freekake_api/freekake_api/settings.py | 1 + freekake_api/freekake_api/urls.py | 1 + freekake_api/mission/__init__.py | 0 freekake_api/mission/admin.py | 3 + freekake_api/mission/apps.py | 6 + freekake_api/mission/models.py | 99 ++++++++++++++++ freekake_api/mission/serializers.py | 138 ++++++++++++++++++++++ freekake_api/mission/tests.py | 3 + freekake_api/mission/urls.py | 14 +++ freekake_api/mission/views.py | 82 +++++++++++++ 11 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 freekake_api/mission/__init__.py create mode 100644 freekake_api/mission/admin.py create mode 100644 freekake_api/mission/apps.py create mode 100644 freekake_api/mission/models.py create mode 100644 freekake_api/mission/serializers.py create mode 100644 freekake_api/mission/tests.py create mode 100644 freekake_api/mission/urls.py create mode 100644 freekake_api/mission/views.py diff --git a/freekake_api/entertainment/serializers.py b/freekake_api/entertainment/serializers.py index dec8381..852ea33 100644 --- a/freekake_api/entertainment/serializers.py +++ b/freekake_api/entertainment/serializers.py @@ -66,7 +66,7 @@ class MangaDetailSerializer(serializers.ModelSerializer): return dict(models.Manga.MANGA_GENRE_CHOICES).get(obj.genre) def get_status_display(self, obj): - return dict(models.Manga.CONTENT_STATUS_CHOICES).get(obj.status, obj.status) + return dict(models.Manga.MANGA_STATUS_CHOICES).get(obj.status, obj.status) class MangaChapterSerializer(serializers.ModelSerializer): diff --git a/freekake_api/freekake_api/settings.py b/freekake_api/freekake_api/settings.py index 2e517bb..3e9429f 100644 --- a/freekake_api/freekake_api/settings.py +++ b/freekake_api/freekake_api/settings.py @@ -52,6 +52,7 @@ INSTALLED_APPS = [ 'content', 'character', 'entertainment', + 'mission', ] MIDDLEWARE = [ diff --git a/freekake_api/freekake_api/urls.py b/freekake_api/freekake_api/urls.py index c7f306a..9f4993b 100644 --- a/freekake_api/freekake_api/urls.py +++ b/freekake_api/freekake_api/urls.py @@ -31,5 +31,6 @@ urlpatterns = [ path('content/', include('content.urls')), path('character/', include('character.urls')), path('entertainment/', include('entertainment.urls')), + path('mission/', include('mission.urls')), ]+ debug_toolbar_urls() + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/freekake_api/mission/__init__.py b/freekake_api/mission/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freekake_api/mission/admin.py b/freekake_api/mission/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/freekake_api/mission/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/freekake_api/mission/apps.py b/freekake_api/mission/apps.py new file mode 100644 index 0000000..116aafc --- /dev/null +++ b/freekake_api/mission/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MissionConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'mission' diff --git a/freekake_api/mission/models.py b/freekake_api/mission/models.py new file mode 100644 index 0000000..d93caca --- /dev/null +++ b/freekake_api/mission/models.py @@ -0,0 +1,99 @@ +from django.db import models +from django.core.exceptions import ValidationError +from django_softdelete.models import SoftDeleteModel +from PIL import Image + +def validate_image_size(image): + if image.size > 1 * 1024 * 1024: + raise ValidationError("File size exceeds 1MB.") + +def validate_image_ext(image): + allowed_extensions = ['png', 'jpg', 'jpeg'] + ext = image.name.split('.')[-1].lower() + if ext not in allowed_extensions: + raise ValidationError("Invalid file type. Only PNG or JPEG allowed.") + +def validate_image(image): + try: + img = Image.open(image) + img.verify() + except Exception: + raise ValidationError("Invalid image file.") + +def validate_file_size(image): + if image.size > 1 * 1024 * 1024: + raise ValidationError("File size exceeds 1MB.") + +class Mission(SoftDeleteModel): + + MISSION_CATEGORY_CHOICES = [ + ('daily','Harian'), + ('weekly', 'Mingguan'), + ('monthly', 'Bulanan'), + ] + + MISSION_TASK_CHOICES = [ + ('scan-qr','Scan QR'), + ('upload-photo','Upload Foto'), + ] + + MISSION_STATUS_CHOICES = [ + ('draft', 'Draf'), + ('published', 'Dipublikasikan'), + ('archived', 'Diarsipkan'), + ] + + name = models.CharField(max_length=255, null=False) + description = models.CharField(max_length=1000, null=True, blank=True) + + featured_image = models.ImageField( + max_length=255, + upload_to="uploads/missions/", + validators=[validate_image_size, validate_image_ext, validate_image], + null=False, blank=False) + + coin = models.IntegerField() + point = models.IntegerField() + + category = models.CharField(max_length=50, choices=MISSION_CATEGORY_CHOICES) + task = models.CharField(max_length=50, choices=MISSION_TASK_CHOICES) + + date_valid = models.DateField(null=True, blank=True) + + time_from_valid = models.TimeField(null=True, blank=True) + time_to_valid = models.TimeField(null=True, blank=True) + + status = models.CharField(max_length=50, choices=MISSION_STATUS_CHOICES, default='draft') + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + posted_at = models.DateTimeField(null=True, blank=True) + archived_at = models.DateTimeField(null=True, blank=True) + +class MissionLog(SoftDeleteModel): + MISSION_LOG_STATUS_CHOICES = [ + ('visited', 'Baru Dikunjungi'), + ('completed', 'Sudah Selesai'), + ('claimed', 'Hadiah Sudah Diambil'), + ] + + mission = models.ForeignKey(Mission, related_name='logs', on_delete=models.CASCADE) + user_id = models.IntegerField() + + status = models.CharField(max_length=50, choices=MISSION_LOG_STATUS_CHOICES) + + coin = models.IntegerField() + point = models.IntegerField() + + image_log = models.ImageField( + max_length=255, + upload_to="uploads/mission_logs/", + validators=[validate_image_size, validate_image_ext, validate_image], + null=False, blank=False) + + completed_at = models.DateTimeField(null=True, blank=True) + claimed_at = models.DateTimeField(null=True, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) \ No newline at end of file diff --git a/freekake_api/mission/serializers.py b/freekake_api/mission/serializers.py new file mode 100644 index 0000000..335b186 --- /dev/null +++ b/freekake_api/mission/serializers.py @@ -0,0 +1,138 @@ +from django.forms import ImageField, FileField +from rest_framework import serializers +from mission import models + +class MissionSerializer(serializers.ModelSerializer): + featured_image = ImageField(max_length=255, allow_empty_file=False) + + class Meta: + model = models.Mission + fields = [ + 'id', + 'name', + 'description', + 'featured_image', + 'coin', + 'point', + 'category', + 'task', + 'status', + 'date_valid', + 'time_from_valid', + 'time_to_valid', + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + request_method = self.context.get('request').method if self.context.get('request') else None + + if request_method in ['PUT', 'PATCH']: + self.fields['featured_image'].required = False + +class MissionDetailSerializer(serializers.ModelSerializer): + featured_image = ImageField(max_length=255, allow_empty_file=False) + category_display = serializers.SerializerMethodField() + task_display = serializers.SerializerMethodField() + status_display = serializers.SerializerMethodField() + + class Meta: + model = models.Mission + fields = [ + 'id', + 'name', + 'description', + 'featured_image', + 'coin', + 'point', + 'category', + 'task', + 'status', + 'date_valid', + 'time_from_valid', + 'time_to_valid', + 'category_display', + 'task_display', + 'status_display', + ] + + def validate(self, data): + category = data.get('category') + task = data.get('task') + + allowed_categories = dict(models.Mission.MISSION_CATEGORY_CHOICES).get(category, []) + valid_category_keys = [key for key, _ in allowed_categories] + + if category not in valid_category_keys: + raise serializers.ValidationError({"category": "Invalid category."}) + + allowed_tasks = dict(models.Mission.MISSION_TASK_CHOICES).get(task, []) + valid_task_keys = [key for key, _ in allowed_tasks] + + if task not in valid_task_keys: + raise serializers.ValidationError({"task": "Invalid task."}) + + return data + + def get_category_display(self, obj): + return obj.get_category_display() + + def get_task_display(self, obj): + return obj.get_task_display() + + def get_status_display(self, obj): + return obj.get_status_display() + +class MissionLogSerializer(serializers.ModelSerializer): + image_log = ImageField(max_length=255, allow_empty_file=False) + + class Meta: + model = models.MissionLog + fields = [ + 'mission', + 'user_id', + 'status', + 'coin', + 'point', + 'image_log', + 'completed_at', + 'claimed_at', + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + request_method = self.context.get('request').method if self.context.get('request') else None + + if request_method in ['PUT', 'PATCH']: + self.fields['image_log'].required = False + +class MissionLogDetailSerializer(serializers.ModelSerializer): + image_log = ImageField(max_length=255, allow_empty_file=False) + status_display = serializers.SerializerMethodField() + + class Meta: + model = models.MissionLog + fields = [ + 'id', + 'mission', + 'user_id', + 'status', + 'coin', + 'point', + 'image_log', + 'completed_at', + 'claimed_at', + 'status_display', + ] + + def validate(self, data): + status = data.get('status') + + allowed_statuses = dict(models.MissionLog.MISSION_LOG_STATUS_CHOICES).keys() + + if status not in allowed_statuses: + raise serializers.ValidationError({"status": "Invalid status."}) + + return data + + def get_status_display(self, obj): + return obj.get_status_display() \ No newline at end of file diff --git a/freekake_api/mission/tests.py b/freekake_api/mission/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/freekake_api/mission/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/freekake_api/mission/urls.py b/freekake_api/mission/urls.py new file mode 100644 index 0000000..c447140 --- /dev/null +++ b/freekake_api/mission/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from mission import views + +urlpatterns = [ + path('missions/', views.MissionList.as_view()), + path('missions//', views.MissionDetail.as_view()), + + path('categories/', views.MissionCategoryChoices.as_view()), + path('tasks/', views.MissionTaskChoices.as_view()), + path('statuses/', views.MissionStatusChoices.as_view()), + + path('mission-logs/', views.MissionLogList.as_view()), + path('mission-logs//', views.MissionLogDetail.as_view()), +] diff --git a/freekake_api/mission/views.py b/freekake_api/mission/views.py new file mode 100644 index 0000000..21df91c --- /dev/null +++ b/freekake_api/mission/views.py @@ -0,0 +1,82 @@ +from rest_framework.response import Response +from rest_framework import generics, filters, views +from django_filters.rest_framework import DjangoFilterBackend, FilterSet, CharFilter, ChoiceFilter + +from mission import models, serializers + +class MissionList(generics.ListCreateAPIView): + queryset = models.Mission.objects.all() + serializer_class = serializers.MissionSerializer + filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] + search_fields = ['name'] + filterset_fields = ['category', 'task', 'status'] + ordering_fields = '__all__' + + def get_serializer_class(self): + if self.request.method in ['GET']: + return serializers.MissionDetailSerializer + return serializers.MissionSerializer + +class MissionDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = models.Mission.objects.all() + serializer_class = serializers.MissionDetailSerializer + + def get_serializer_class(self): + if self.request.method in ['GET']: + return serializers.MissionDetailSerializer + return serializers.MissionSerializer + +class MissionCategoryChoices(views.APIView): + def get(self, request, *args, **kwargs): + return Response({ + "count": len(models.Mission.MISSION_CATEGORY_CHOICES), + "results": [ + {"value": choice[0], "label": choice[1]} + for choice in models.Mission.MISSION_CATEGORY_CHOICES + ] + }) + +class MissionTaskChoices(views.APIView): + def get(self, request, *args, **kwargs): + return Response({ + "count": len(models.Mission.MISSION_TASK_CHOICES), + "results": [ + {"value": choice[0], "label": choice[1]} + for choice in models.Mission.MISSION_TASK_CHOICES + ] + }) + +class MissionStatusChoices(views.APIView): + def get(self, request, *args, **kwargs): + return Response({ + "count": len(models.Mission.MISSION_STATUS_CHOICES), + "results": [ + {"value": choice[0], "label": choice[1]} + for choice in models.Mission.MISSION_STATUS_CHOICES + ] + }) + +class MissionLogList(generics.ListCreateAPIView): + queryset = models.MissionLog.objects.all() + serializer_class = serializers.MissionLogSerializer + filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] + search_fields = ['user_id'] + filterset_fields = ['mission', 'user_id', 'status'] + ordering_fields = '__all__' + + def get_serializer_class(self): + if self.request.method in ['GET']: + return serializers.MissionLogDetailSerializer + return serializers.MissionLogSerializer + +class MissionLogDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = models.MissionLog.objects.all() + serializer_class = serializers.MissionLogSerializer + + def get_serializer_class(self): + serializer_class = self.serializer_class + + if self.request.method in ['GET']: + return serializers.MissionLogDetailSerializer + + return serializer_class \ No newline at end of file