diff --git a/freekake_api/content/models.py b/freekake_api/content/models.py index 96613b3..1b99cc6 100644 --- a/freekake_api/content/models.py +++ b/freekake_api/content/models.py @@ -142,7 +142,7 @@ class Media(SoftDeleteModel): name = models.CharField(max_length=255, null=False) media = models.FileField( max_length=255, - upload_to="uploads/media/", + upload_to="uploads/media/contents/", validators=[validate_file_size], null=False, blank=False) diff --git a/freekake_api/content/serializers.py b/freekake_api/content/serializers.py index 4fadf4d..97b0fae 100644 --- a/freekake_api/content/serializers.py +++ b/freekake_api/content/serializers.py @@ -2,7 +2,6 @@ from django.forms import ImageField, FileField from rest_framework import serializers from content import models - class ContentSerializer(serializers.ModelSerializer): featured_image = ImageField(max_length=255, allow_empty_file=False) diff --git a/freekake_api/entertainment/__init__.py b/freekake_api/entertainment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freekake_api/entertainment/admin.py b/freekake_api/entertainment/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/freekake_api/entertainment/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/freekake_api/entertainment/apps.py b/freekake_api/entertainment/apps.py new file mode 100644 index 0000000..824e679 --- /dev/null +++ b/freekake_api/entertainment/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EntertainmentsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'entertainment' diff --git a/freekake_api/entertainment/models.py b/freekake_api/entertainment/models.py new file mode 100644 index 0000000..3242aca --- /dev/null +++ b/freekake_api/entertainment/models.py @@ -0,0 +1,101 @@ +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 Manga(SoftDeleteModel): + + MANGA_GENRE_CHOICES = [ + ('komedi','Komedi'), + ('petualangan', 'Petualangan'), + ('fiksi-ilmiah', 'Fiksi Ilmiah'), + ('fantasi', 'Fantasi'), + ('slice-of-life', 'Kehidupan Sehari-hari'), + ] + + MANGA_STATUS_CHOICES = [ + ('draft', 'Draf'), + ('published', 'Dipublikasikan'), + ('archived', 'Diarsipkan'), + ] + + title = models.CharField(max_length=255, null=False) + slug = models.SlugField(max_length=255, unique=True) + + featured_image = models.ImageField( + max_length=255, + upload_to="uploads/entertainments/", + validators=[validate_image_size, validate_image_ext, validate_image], + null=False, blank=False) + + genre = models.CharField(max_length=50, choices=MANGA_GENRE_CHOICES) + + synopsis = models.CharField(max_length=1000, null=True, blank=True) + + coin = models.IntegerField() + + color = models.CharField(max_length=7, null=True, blank=True) + + status = models.CharField(max_length=50, choices=MANGA_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) + + def __str__(self): + return self.title + +class MangaChapter(SoftDeleteModel): + + manga = models.ForeignKey(Manga, related_name='chapters', on_delete=models.CASCADE) + + chapter = models.PositiveSmallIntegerField(null=False) + + status = models.CharField(max_length=50, choices=Manga.MANGA_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) + + def __str__(self): + return self.manga.title + " - Chapter: " + str(self.chapter) + +class MangaChapterPage(SoftDeleteModel): + + chapter = models.ForeignKey(MangaChapter, related_name='media', on_delete=models.CASCADE) + + order = models.PositiveSmallIntegerField(null=False) + + media = models.FileField( + max_length=255, + upload_to="uploads/entertainments/manga/chapters/media/", + validators=[validate_file_size], + null=False, blank=False) + + def __str__(self): + return self.chapter.manga.title + " - Chapter: " + self.chapter.chapter + " - Page: " + str(self.order) diff --git a/freekake_api/entertainment/serializers.py b/freekake_api/entertainment/serializers.py new file mode 100644 index 0000000..dec8381 --- /dev/null +++ b/freekake_api/entertainment/serializers.py @@ -0,0 +1,103 @@ +from django.forms import ImageField, FileField +from rest_framework import serializers +from entertainment import models + +class MangaSerializer(serializers.ModelSerializer): + featured_image = ImageField(max_length=255, allow_empty_file=False) + + class Meta: + model = models.Manga + fields = [ + 'id', + 'title', + 'slug', + 'featured_image', + 'genre', + 'synopsis', + 'coin', + 'color', + 'status', + 'posted_at', + 'archived_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['featured_image'].required = False + +class MangaDetailSerializer(serializers.ModelSerializer): + featured_image = ImageField(max_length=255, allow_empty_file=False) + genre_display = serializers.SerializerMethodField() + status_display = serializers.SerializerMethodField() + + class Meta: + model = models.Manga + fields = [ + 'id', + 'title', + 'slug', + 'featured_image', + 'genre', + 'synopsis', + 'coin', + 'color', + 'status', + 'posted_at', + 'archived_at', + 'genre_display', + 'status_display', + ] + + def validate(self, data): + genre = data.get('genre') + + allowed_genres = dict(models.Manga.MANGA_GENRE_CHOICES).get(genre, []) + valid_genre_keys = [key for key, _ in allowed_genres] + + if genre not in valid_genre_keys: + raise serializers.ValidationError({'topic': f'Genre "{genre}" is not valid for genre "{genre}"'}) + + return data + + def get_genre_display(self, obj): + 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) + +class MangaChapterSerializer(serializers.ModelSerializer): + + class Meta: + model = models.MangaChapter + fields = ['id', 'manga', 'chapter', 'status', 'posted_at', 'archived_at'] + +class MangaChapterDetailSerializer(serializers.ModelSerializer): + manga = MangaSerializer(read_only=True) + + class Meta: + model = models.MangaChapter + fields = ['id', 'manga', 'chapter', 'status', 'posted_at', 'archived_at'] + +class MangaChapterPageSerializer(serializers.ModelSerializer): + media = FileField(max_length=255, allow_empty_file=False) + + class Meta: + model = models.MangaChapterPage + fields = ['id', 'chapter', 'order', 'media'] + + 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['media'].required = False + +class MangaChapterPageDetailSerializer(serializers.ModelSerializer): + chapter = MangaChapterDetailSerializer(read_only=True) + + class Meta: + model = models.MangaChapterPage + fields = ['id', 'chapter', 'order', 'media'] \ No newline at end of file diff --git a/freekake_api/entertainment/tests.py b/freekake_api/entertainment/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/freekake_api/entertainment/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/freekake_api/entertainment/urls.py b/freekake_api/entertainment/urls.py new file mode 100644 index 0000000..e6d8698 --- /dev/null +++ b/freekake_api/entertainment/urls.py @@ -0,0 +1,16 @@ +from django.urls import path +from entertainment import views + +urlpatterns = [ + path('manga/', views.MangaList.as_view()), + path('manga//', views.MangaDetail.as_view()), + + path('genres/', views.MangaGenreChoices.as_view()), + path('statuses/', views.MangaStatusChoices.as_view()), + + path('manga//chapters/', views.MangaChapterList.as_view()), + path('manga//chapters//', views.MangaChapterDetail.as_view()), + + path('manga//chapters//pages/', views.MangaChapterPageList.as_view()), + path('manga//chapters//pages//', views.MangaChapterPageDetail.as_view()), +] diff --git a/freekake_api/entertainment/views.py b/freekake_api/entertainment/views.py new file mode 100644 index 0000000..7e6d8c0 --- /dev/null +++ b/freekake_api/entertainment/views.py @@ -0,0 +1,91 @@ +from rest_framework.response import Response +from rest_framework import generics, filters, views +from django_filters.rest_framework import DjangoFilterBackend, FilterSet, CharFilter, ChoiceFilter + +from entertainment import models, serializers + +class MangaList(generics.ListCreateAPIView): + queryset = models.Manga.objects.all() + serializer_class = serializers.MangaSerializer + filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] + search_fields = ['title'] + filterset_fields = ['genre', 'status'] + ordering_fields = '__all__' + + def get_serializer_class(self): + if self.request.method in ['GET']: + return serializers.MangaDetailSerializer + return serializers.MangaSerializer + +class MangaDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = models.Manga.objects.all() + serializer_class = serializers.MangaDetailSerializer + + def get_serializer_class(self): + if self.request.method in ['GET']: + return serializers.MangaDetailSerializer + return serializers.MangaSerializer + +class MangaGenreChoices(views.APIView): + def get(self, request, *args, **kwargs): + return Response({ + "count": len(models.Manga.MANGA_GENRE_CHOICES), + "results": [ + {"value": choice[0], "label": choice[1]} + for choice in models.Manga.MANGA_GENRE_CHOICES + ] + }) + +class MangaStatusChoices(views.APIView): + def get(self, request, *args, **kwargs): + return Response({ + "count": len(models.Manga.MANGA_STATUS_CHOICES), + "results": [ + {"value": choice[0], "label": choice[1]} + for choice in models.Manga.MANGA_STATUS_CHOICES + ] + }) + +class MangaChapterList(generics.ListCreateAPIView): + serializer_class = serializers.MangaChapterSerializer + ordering_fields = ['chapter'] + + def get_queryset(self): + manga_id = self.kwargs['manga_id'] + return models.MangaChapter.objects.filter(manga_id=manga_id) + + def get_serializer_class(self): + if self.request.method in ['GET']: + return serializers.MangaChapterDetailSerializer + return serializers.MangaChapterSerializer + +class MangaChapterDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = models.MangaChapter.objects.all() + serializer_class = serializers.MangaChapterDetailSerializer + + def get_serializer_class(self): + if self.request.method in ['GET']: + return serializers.MangaChapterDetailSerializer + return serializers.MangaChapterSerializer + +class MangaChapterPageList(generics.ListCreateAPIView): + serializer_class = serializers.MangaChapterPageSerializer + ordering_fields = ['order'] + + def get_queryset(self): + chapter_id = self.kwargs['chapter_id'] + return models.MangaChapterPage.objects.filter(chapter_id=chapter_id) + + def get_serializer_class(self): + if self.request.method in ['GET']: + return serializers.MangaChapterPageDetailSerializer + return serializers.MangaChapterPageSerializer + +class MangaChapterPageDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = models.MangaChapterPage.objects.all() + serializer_class = serializers.MangaChapterPageDetailSerializer + + def get_serializer_class(self): + if self.request.method in ['GET']: + return serializers.MangaChapterPageDetailSerializer + return serializers.MangaChapterPageSerializer \ No newline at end of file diff --git a/freekake_api/freekake_api/settings.py b/freekake_api/freekake_api/settings.py index 50e5240..3804214 100644 --- a/freekake_api/freekake_api/settings.py +++ b/freekake_api/freekake_api/settings.py @@ -51,6 +51,7 @@ INSTALLED_APPS = [ 'core', 'content', 'character', + 'entertainment', ] MIDDLEWARE = [ @@ -200,6 +201,9 @@ SIMPLE_JWT = { DJOSER = { 'TOKEN_MODEL': None, + 'SEND_ACTIVATION_EMAIL': True, + 'ACTIVATION_URL': 'accounts/activation/{uid}/{token}/', + "SEND_CONFIRMATION_EMAIL": True, } SIMPLE_JWT = { @@ -210,3 +214,8 @@ SIMPLE_JWT = { "ISSUER": 'freekake_api', } +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'sandbox.smtp.mailtrap.io' +EMAIL_HOST_USER = 'a14db434093c11' +EMAIL_HOST_PASSWORD = '3524293f1ca830' +EMAIL_PORT = '2525' diff --git a/freekake_api/freekake_api/urls.py b/freekake_api/freekake_api/urls.py index 0627513..c7f306a 100644 --- a/freekake_api/freekake_api/urls.py +++ b/freekake_api/freekake_api/urls.py @@ -30,4 +30,6 @@ urlpatterns = [ path('core/', include('core.urls')), path('content/', include('content.urls')), path('character/', include('character.urls')), + path('entertainment/', include('entertainment.urls')), + ]+ debug_toolbar_urls() + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)