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.")
def validate_image_ext(image):
allowed_extensions = ['png']
allowed_extensions = ['png', 'jpg', 'jpeg']
ext = image.name.split('.')[-1].lower()
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):
try:
@ -20,57 +20,69 @@ def validate_image(image):
except Exception:
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):
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 = [
('Text', 'Teks'),
('Infographic', 'Infografis'),
('Image', 'Gambar'),
('Video', 'Video'),
('Branching Scenario', 'Pilihan Bercabang'),
('Crossword', 'Teka-teki Silang'),
('Drag And Drop', 'Pencocokan'),
('Find the Words', 'Cari Kata'),
('Memory Game', 'Permainan Memori'),
('Personality Quiz', 'Permainan Tebak Sifat'),
('Game Map', 'Peta Permainan'),
('Multiple Choice', 'Pilihan Ganda'),
('teks','Teks'),
('infografis', 'Infografis'),
('gambar', 'Gambar'),
('video', 'Video'),
('branching-scenario', 'Pilihan Bercabang'),
('tts', 'Teka-teki Silang'),
('pencocokan', 'Pencocokan'),
('cari-kata', 'Cari Kata'),
('memory-game', 'Permainan Memori'),
('personality-quiz', 'Permainan Tebak Sifat'),
('game-map', 'Peta Permainan'),
('pilihan-ganda', 'Pilihan Ganda'),
]
title = models.CharField(max_length=255, null=False)
@ -82,8 +94,8 @@ class Content(SoftDeleteModel):
validators=[validate_image_size, validate_image_ext, validate_image],
null=False, blank=False)
theme = models.ForeignKey(ContentTheme, on_delete=models.CASCADE, null=False)
topic = models.ForeignKey(ContentTopic, on_delete=models.CASCADE, null=False)
theme = models.CharField(max_length=25, choices=CONTENT_THEME_CHOICES)
topic = models.CharField(max_length=25)
format = models.CharField(max_length=25, choices=CONTENT_FORMAT_CHOICES)
description = models.CharField(max_length=255, null=True, blank=True)

View File

@ -2,26 +2,6 @@ from django.forms import ImageField
from rest_framework import serializers
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):
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']
class ContentDetailSerializer(serializers.ModelSerializer):
theme = ContentThemeSerializer(read_only=True)
topic = ContentTopicDetailSerializer(read_only=True)
featured_image = ImageField(max_length=255, allow_empty_file=False)
class Meta:
model = models.Content
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/<int:pk>/', views.ContentDetail.as_view()),
path('themes/', views.ContentThemeList.as_view()),
path('themes/<int:pk>/', views.ContentThemeDetail.as_view()),
path('topics/', views.ContentTopicList.as_view()),
path('topics/<int:pk>/', views.ContentTopicDetail.as_view()),
path('themes/', views.ContentThemeChoices.as_view()),
path('topics/<str:theme_key>/', views.ContentTopicChoices.as_view()),
path('formats/', views.ContentFormatChoices.as_view()),
]

View File

@ -49,44 +49,32 @@ class ContentDetail(generics.RetrieveUpdateDestroyAPIView):
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 ContentThemeChoices(views.APIView):
def get(self, request, *args, **kwargs):
return Response({
"count": len(models.Content.CONTENT_THEME_CHOICES),
"results": [
{"value": choice[0], "label": choice[1]}
for choice in models.Content.CONTENT_THEME_CHOICES
]
})
class ContentThemeDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = models.ContentTheme.objects.all()
serializer_class = serializers.ContentThemeSerializer
class ContentTopicChoices(views.APIView):
def get(self, request, theme_key):
topics = models.Content.CONTENT_TOPIC_CHOICES.get(theme_key)
if not topics:
return Response({
"count": 0,
"results": []
})
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):
queryset = models.ContentTopic.objects.all()
serializer_class = serializers.ContentTopicSerializer
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'GET':
serializer_class = serializers.ContentTopicDetailSerializer
return serializer_class
return Response({
"count": len(topics),
"results": [
{"value": choice[0], "label": choice[1]}
for choice in topics
]
})
class ContentFormatChoices(views.APIView):
def get(self, request, *args, **kwargs):