content, ubah theme dan topic dengan enum
This commit is contained in:
parent
11ca62eee4
commit
b50cded82f
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
@ -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()),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user