This commit is contained in:
Irwan Cahyono 2025-10-08 11:39:49 +07:00
parent 0216751834
commit add9caf0f2
11 changed files with 348 additions and 1 deletions

View File

@ -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):

View File

@ -52,6 +52,7 @@ INSTALLED_APPS = [
'content',
'character',
'entertainment',
'mission',
]
MIDDLEWARE = [

View File

@ -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)

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MissionConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'mission'

View File

@ -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)

View File

@ -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()

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,14 @@
from django.urls import path
from mission import views
urlpatterns = [
path('missions/', views.MissionList.as_view()),
path('missions/<int:pk>/', 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/<int:pk>/', views.MissionLogDetail.as_view()),
]

View File

@ -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