content, ubah theme dan topic dengan enum

This commit is contained in:
Irwan Cahyono 2025-07-07 23:01:31 +07:00
parent 11ca62eee4
commit b50cded82f
4 changed files with 102 additions and 116 deletions

View File

@ -8,10 +8,10 @@ def validate_image_size(image):
raise ValidationError("File size exceeds 1MB.") raise ValidationError("File size exceeds 1MB.")
def validate_image_ext(image): def validate_image_ext(image):
allowed_extensions = ['png'] allowed_extensions = ['png', 'jpg', 'jpeg']
ext = image.name.split('.')[-1].lower() ext = image.name.split('.')[-1].lower()
if ext not in allowed_extensions: if ext not in allowed_extensions:
raise ValidationError("Invalid file type. Only PNG allowed.") raise ValidationError("Invalid file type. Only PNG or JPEG allowed.")
def validate_image(image): def validate_image(image):
try: try:
@ -20,57 +20,69 @@ def validate_image(image):
except Exception: except Exception:
raise ValidationError("Invalid image file.") raise ValidationError("Invalid image file.")
class ContentTheme(SoftDeleteModel):
theme = models.CharField(max_length=255, null=False)
description = models.CharField(max_length=1000)
featured_image = models.ImageField(
max_length=255,
upload_to="uploads/content_themes/",
validators=[validate_image_size, validate_image_ext, validate_image],
null=False, blank=False)
color = models.CharField(max_length=7, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.theme
class ContentTopic(SoftDeleteModel):
topic = models.CharField(max_length=255, null=False)
description = models.CharField(max_length=1000)
featured_image = models.ImageField(
max_length=255,
upload_to="uploads/content_topics/",
validators=[validate_image_size, validate_image_ext, validate_image],
null=False, blank=False)
theme = models.ForeignKey(ContentTheme, on_delete=models.CASCADE, null=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.topic
class Content(SoftDeleteModel): class Content(SoftDeleteModel):
CONTENT_THEME_CHOICES = [
('furikake', 'Furikake'),
('gizi', 'Gizi'),
('kesehatan', 'Kesehatan'),
('pendidikan', 'Pendidikan'),
('keselamatan', 'Keselamatan'),
]
CONTENT_TOPIC_CHOICES = {
'furikake': [
('sejarah-furikake', 'Sejarah Furikake'),
('kandungan-furikake', 'Kandungan Furikake'),
('pembuatan-furikake', 'Pembuatan Furikake'),
('menikmati-furikake', 'Menikmati Furikake')
],
'gizi': [
('gizi-seimbang', 'Gizi Seimbang'),
('kecukupan-gizi', 'Kecukupan Gizi'),
('kualitas-pangan', 'Kualitas Bahan Pangan'),
('budaya-tradisi-pangan', 'Budaya dan Tradisi Pangan'),
('ketersediaan-aksesibilitas-pangan', 'Ketersediaan dan Aksesibilitas Gizi dan Pangan')
],
'kesehatan': [
('olahraga', 'Olahraga'),
('waktu-tidur', 'Waktu Tidur'),
('kesehatan-mental', 'Kesehatan Mental'),
('pendidikan-seksua', 'Pendidikan Seksual'),
('rokok-alkohol-narkoba', 'Bahaya Merokok dan Alkohol serta Pencegahan Narkoba'),
('sistem-imun', 'Sistem Imun')
],
'pendidikan': [
('berpikir-kreatif', 'Berpikir Kreatif'),
('berpikir-kritis', 'Berpikir Kritis'),
('pentingnya-membaca', 'Pentingnya Membaca'),
('literasi-digital', 'Literasi Digital'),
('kesadaran-lingkungan', 'Kesadaran Lingkungan'),
('pancasila', 'Pancasila'),
('biologi', 'Biologi'),
('aritmatika', 'Aritmatika'),
('bahasa-indonesia', 'Bahasa Indonesia'),
],
'keselamatan': [
('lalu-lintas', 'Keselamatan Berlalu Lintas'),
('pertolongan-pertama', 'Pertolongan Petrtama'),
('bencana', 'Kesiapsiagaan Bencana')
]
}
CONTENT_FORMAT_CHOICES = [ CONTENT_FORMAT_CHOICES = [
('Text', 'Teks'), ('teks','Teks'),
('Infographic', 'Infografis'), ('infografis', 'Infografis'),
('Image', 'Gambar'), ('gambar', 'Gambar'),
('Video', 'Video'), ('video', 'Video'),
('Branching Scenario', 'Pilihan Bercabang'), ('branching-scenario', 'Pilihan Bercabang'),
('Crossword', 'Teka-teki Silang'), ('tts', 'Teka-teki Silang'),
('Drag And Drop', 'Pencocokan'), ('pencocokan', 'Pencocokan'),
('Find the Words', 'Cari Kata'), ('cari-kata', 'Cari Kata'),
('Memory Game', 'Permainan Memori'), ('memory-game', 'Permainan Memori'),
('Personality Quiz', 'Permainan Tebak Sifat'), ('personality-quiz', 'Permainan Tebak Sifat'),
('Game Map', 'Peta Permainan'), ('game-map', 'Peta Permainan'),
('Multiple Choice', 'Pilihan Ganda'), ('pilihan-ganda', 'Pilihan Ganda'),
] ]
title = models.CharField(max_length=255, null=False) title = models.CharField(max_length=255, null=False)
@ -82,8 +94,8 @@ class Content(SoftDeleteModel):
validators=[validate_image_size, validate_image_ext, validate_image], validators=[validate_image_size, validate_image_ext, validate_image],
null=False, blank=False) null=False, blank=False)
theme = models.ForeignKey(ContentTheme, on_delete=models.CASCADE, null=False) theme = models.CharField(max_length=25, choices=CONTENT_THEME_CHOICES)
topic = models.ForeignKey(ContentTopic, on_delete=models.CASCADE, null=False) topic = models.CharField(max_length=25)
format = models.CharField(max_length=25, choices=CONTENT_FORMAT_CHOICES) format = models.CharField(max_length=25, choices=CONTENT_FORMAT_CHOICES)
description = models.CharField(max_length=255, null=True, blank=True) description = models.CharField(max_length=255, null=True, blank=True)

View File

@ -2,27 +2,7 @@ from django.forms import ImageField
from rest_framework import serializers from rest_framework import serializers
from content import models from content import models
class ContentThemeSerializer(serializers.ModelSerializer):
featured_image = ImageField(max_length=255, allow_empty_file=False)
class Meta:
model = models.ContentTheme
fields = ['id', 'theme', 'description', 'featured_image', 'color']
class ContentTopicSerializer(serializers.ModelSerializer):
featured_image = ImageField(max_length=255, allow_empty_file=False)
class Meta:
model = models.ContentTopic
fields = ['id', 'topic', 'description', 'featured_image', 'theme']
class ContentTopicDetailSerializer(serializers.ModelSerializer):
theme = ContentThemeSerializer(read_only=True)
featured_image = ImageField(max_length=255, allow_empty_file=False)
class Meta:
model = models.ContentTopic
fields = ['id', 'topic', 'description', 'featured_image', 'theme']
class ContentSerializer(serializers.ModelSerializer): class ContentSerializer(serializers.ModelSerializer):
featured_image = ImageField(max_length=255, allow_empty_file=False) featured_image = ImageField(max_length=255, allow_empty_file=False)
@ -31,10 +11,20 @@ class ContentSerializer(serializers.ModelSerializer):
fields = ['id', 'title', 'slug', 'featured_image', 'theme', 'topic', 'format', 'description','content', 'point', 'coin', 'data', 'grades', 'color'] fields = ['id', 'title', 'slug', 'featured_image', 'theme', 'topic', 'format', 'description','content', 'point', 'coin', 'data', 'grades', 'color']
class ContentDetailSerializer(serializers.ModelSerializer): class ContentDetailSerializer(serializers.ModelSerializer):
theme = ContentThemeSerializer(read_only=True)
topic = ContentTopicDetailSerializer(read_only=True)
featured_image = ImageField(max_length=255, allow_empty_file=False) featured_image = ImageField(max_length=255, allow_empty_file=False)
class Meta: class Meta:
model = models.Content model = models.Content
fields = ['id', 'title', 'slug', 'featured_image', 'theme', 'topic', 'format', 'description','content', 'point', 'coin', 'data', 'grades', 'color'] fields = ['id', 'title', 'slug', 'featured_image', 'theme', 'topic', 'format', 'description','content', 'point', 'coin', 'data', 'grades', 'color']
def validate(self, data):
theme = data.get('theme')
topic = data.get('topic')
allowed_topics = dict(models.Content.CONTENT_TOPIC_CHOICES).get(theme, [])
valid_topic_keys = [key for key, _ in allowed_topics]
if topic not in valid_topic_keys:
raise serializers.ValidationError({'topic': f'Topic "{topic}" is not valid for theme "{theme}"'})
return data

View File

@ -5,11 +5,7 @@ urlpatterns = [
path('contents/', views.ContentList.as_view()), path('contents/', views.ContentList.as_view()),
path('contents/<int:pk>/', views.ContentDetail.as_view()), path('contents/<int:pk>/', views.ContentDetail.as_view()),
path('themes/', views.ContentThemeList.as_view()), path('themes/', views.ContentThemeChoices.as_view()),
path('themes/<int:pk>/', views.ContentThemeDetail.as_view()), path('topics/<str:theme_key>/', views.ContentTopicChoices.as_view()),
path('topics/', views.ContentTopicList.as_view()),
path('topics/<int:pk>/', views.ContentTopicDetail.as_view()),
path('formats/', views.ContentFormatChoices.as_view()), path('formats/', views.ContentFormatChoices.as_view()),
] ]

View File

@ -48,45 +48,33 @@ class ContentDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.ContentDetailSerializer serializer_class = serializers.ContentDetailSerializer
return serializer_class return serializer_class
class ContentThemeList(generics.ListCreateAPIView):
queryset = models.ContentTheme.objects.all()
serializer_class = serializers.ContentThemeSerializer
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ['theme']
ordering_fields = '__all__'
class ContentThemeDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = models.ContentTheme.objects.all()
serializer_class = serializers.ContentThemeSerializer
class ContentTopicList(generics.ListCreateAPIView):
queryset = models.ContentTopic.objects.all()
serializer_class = serializers.ContentTopicSerializer
filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend]
search_fields = ['topic']
filterset_fields = ['theme']
ordering_fields = '__all__'
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'GET':
serializer_class = serializers.ContentTopicDetailSerializer
return serializer_class
class ContentTopicDetail(generics.RetrieveUpdateDestroyAPIView): class ContentThemeChoices(views.APIView):
queryset = models.ContentTopic.objects.all() def get(self, request, *args, **kwargs):
serializer_class = serializers.ContentTopicSerializer return Response({
"count": len(models.Content.CONTENT_THEME_CHOICES),
"results": [
{"value": choice[0], "label": choice[1]}
for choice in models.Content.CONTENT_THEME_CHOICES
]
})
def get_serializer_class(self): class ContentTopicChoices(views.APIView):
serializer_class = self.serializer_class def get(self, request, theme_key):
topics = models.Content.CONTENT_TOPIC_CHOICES.get(theme_key)
if not topics:
return Response({
"count": 0,
"results": []
})
if self.request.method == 'GET': return Response({
serializer_class = serializers.ContentTopicDetailSerializer "count": len(topics),
"results": [
return serializer_class {"value": choice[0], "label": choice[1]}
for choice in topics
]
})
class ContentFormatChoices(views.APIView): class ContentFormatChoices(views.APIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):