Compare commits

..

2 Commits

Author SHA1 Message Date
85e674e6d6 file uploads 2025-06-28 22:41:56 +07:00
151cd89b88 debug toolbar dan environment 2025-06-28 21:38:44 +07:00
5 changed files with 86 additions and 31 deletions

View File

@ -1,12 +1,34 @@
from django.db import models from django.db import models
from django.core.exceptions import ValidationError
from django_softdelete.models import SoftDeleteModel from django_softdelete.models import SoftDeleteModel
from django.core.validators import MaxValueValidator, MinValueValidator 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']
ext = image.name.split('.')[-1].lower()
if ext not in allowed_extensions:
raise ValidationError("Invalid file type. Only PNG allowed.")
def validate_image(image):
try:
img = Image.open(image)
img.verify()
except Exception:
raise ValidationError("Invalid image file.")
class ContentTheme(SoftDeleteModel): class ContentTheme(SoftDeleteModel):
theme = models.CharField(max_length=255, null=False) theme = models.CharField(max_length=255, null=False)
description = models.CharField(max_length=1000) description = models.CharField(max_length=1000)
featured_image = 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)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@ -18,7 +40,11 @@ class ContentTopic(SoftDeleteModel):
topic = models.CharField(max_length=255, null=False) topic = models.CharField(max_length=255, null=False)
description = models.CharField(max_length=1000) description = models.CharField(max_length=1000)
featured_image = 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) theme = models.ForeignKey(ContentTheme, on_delete=models.CASCADE, null=False)
@ -30,28 +56,33 @@ class ContentTopic(SoftDeleteModel):
class Content(SoftDeleteModel): class Content(SoftDeleteModel):
class ContentFormat(models.TextChoices): CONTENT_FORMAT_CHOICES = [
TEXT = 'Text', 'Teks' ('Text', 'Teks'),
INFOGRAPHIC = 'Infographic', 'Infografis' ('Infographic', 'Infografis'),
IMAGE = 'Image', 'Gambar' ('Image', 'Gambar'),
VIDEO = 'Video', 'Video' ('Video', 'Video'),
BRANCHING_SCENARIO = 'Branching Scenario', 'Pilihan Bercabang' ('Branching Scenario', 'Pilihan Bercabang'),
CROSSWORD = 'Crossword', 'Teka-teki Silang' ('Crossword', 'Teka-teki Silang'),
DRAG_AND_DROP = 'Drag And Drop', 'Pencocokan' ('Drag And Drop', 'Pencocokan'),
FIND_THE_WORDS = 'Find the Words', 'Cari Kata' ('Find the Words', 'Cari Kata'),
MEMORY_GAME = 'Memory Game', 'Permainan Memori' ('Memory Game', 'Permainan Memori'),
PERSONALITY_QUIZ = 'Personality Quiz', 'Permainan Tebak Sifat' ('Personality Quiz', 'Permainan Tebak Sifat'),
GAME_MAP = 'Game Map', 'Peta Permainan' ('Game Map', 'Peta Permainan'),
MULTIPLE_CHOICE = 'Multiple Choice', 'Pilihan Ganda' ('Multiple Choice', 'Pilihan Ganda'),
]
title = models.CharField(max_length=255, null=False) title = models.CharField(max_length=255, null=False)
slug = models.SlugField(max_length=255) slug = models.SlugField(max_length=255)
featured_image = models.CharField(max_length=1000) featured_image = models.ImageField(
max_length=255,
upload_to="uploads/contents/",
validators=[validate_image_size, validate_image_ext, validate_image],
null=False, blank=False)
theme = models.ForeignKey(ContentTheme, on_delete=models.CASCADE, null=False) theme = models.ForeignKey(ContentTheme, on_delete=models.CASCADE, null=False)
topic = models.ForeignKey(ContentTopic, on_delete=models.CASCADE, null=False) topic = models.ForeignKey(ContentTopic, on_delete=models.CASCADE, null=False)
format = models.CharField(max_length=25, choices=ContentFormat.choices, default=ContentFormat.TEXT) format = models.CharField(max_length=25, choices=CONTENT_FORMAT_CHOICES)
content = models.TextField(null=True) content = models.TextField(null=True)
data = models.JSONField(null=True) data = models.JSONField(null=True)

View File

@ -1,12 +1,15 @@
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): class ContentThemeSerializer(serializers.ModelSerializer):
featured_image = ImageField(max_length=255, allow_empty_file=False)
class Meta: class Meta:
model = models.ContentTheme model = models.ContentTheme
fields = ['id', 'theme', 'description', 'featured_image'] fields = ['id', 'theme', 'description', 'featured_image']
class ContentTopicSerializer(serializers.ModelSerializer): class ContentTopicSerializer(serializers.ModelSerializer):
featured_image = ImageField(max_length=255, allow_empty_file=False)
class Meta: class Meta:
model = models.ContentTopic model = models.ContentTopic
@ -14,12 +17,15 @@ class ContentTopicSerializer(serializers.ModelSerializer):
class ContentTopicDetailSerializer(serializers.ModelSerializer): class ContentTopicDetailSerializer(serializers.ModelSerializer):
theme = ContentThemeSerializer(read_only=True) theme = ContentThemeSerializer(read_only=True)
featured_image = ImageField(max_length=255, allow_empty_file=False)
class Meta: class Meta:
model = models.ContentTopic model = models.ContentTopic
fields = ['id', 'topic', 'description', 'featured_image', 'theme'] 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)
class Meta: class Meta:
model = models.Content model = models.Content
fields = ['id', 'title', 'slug', 'featured_image', 'theme', 'topic', 'format', 'content', 'point', 'coin', 'data', 'grades'] fields = ['id', 'title', 'slug', 'featured_image', 'theme', 'topic', 'format', 'content', 'point', 'coin', 'data', 'grades']
@ -27,6 +33,7 @@ class ContentSerializer(serializers.ModelSerializer):
class ContentDetailSerializer(serializers.ModelSerializer): class ContentDetailSerializer(serializers.ModelSerializer):
theme = ContentThemeSerializer(read_only=True) theme = ContentThemeSerializer(read_only=True)
topic = ContentTopicDetailSerializer(read_only=True) topic = ContentTopicDetailSerializer(read_only=True)
featured_image = ImageField(max_length=255, allow_empty_file=False)
class Meta: class Meta:
model = models.Content model = models.Content

View File

@ -1,5 +1,5 @@
from rest_framework import generics, filters from rest_framework import generics, filters, parsers
from django_filters.rest_framework import DjangoFilterBackend, FilterSet, CharFilter from django_filters.rest_framework import DjangoFilterBackend, FilterSet, CharFilter, ChoiceFilter
from content import models, serializers from content import models, serializers
@ -7,6 +7,7 @@ class ContentFilter(FilterSet):
grade = CharFilter( grade = CharFilter(
method='filter_grades_contains', label="Grade/Class" method='filter_grades_contains', label="Grade/Class"
) )
format_type = ChoiceFilter(choices=models.Content.CONTENT_FORMAT_CHOICES, label="Format", field_name="format")
def filter_grades_contains(self, queryset, name, value): def filter_grades_contains(self, queryset, name, value):
try: try:
@ -17,14 +18,13 @@ class ContentFilter(FilterSet):
class Meta: class Meta:
model = models.Content model = models.Content
fields = ['format', 'theme', 'topic', 'grade'] fields = ['format_type', 'theme', 'topic', 'grade']
class ContentList(generics.ListCreateAPIView): class ContentList(generics.ListCreateAPIView):
queryset = models.Content.objects.all() queryset = models.Content.objects.all()
serializer_class = serializers.ContentSerializer serializer_class = serializers.ContentSerializer
filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend]
search_fields = ['title'] search_fields = ['title']
#filterset_fields = ['format', 'theme', 'topic']
filterset_class = ContentFilter filterset_class = ContentFilter
ordering_fields = '__all__' ordering_fields = '__all__'

View File

@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/5.1/ref/settings/
""" """
from pathlib import Path from pathlib import Path
from decouple import config
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -20,10 +22,10 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-@wf_1z20edr655upw6t3tf==56!t%vk(oky=v4+n0io+om=4^x' SECRET_KEY = config('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []
@ -44,6 +46,7 @@ INSTALLED_APPS = [
'psycopg2', 'psycopg2',
'corsheaders', 'corsheaders',
'django_filters', 'django_filters',
'debug_toolbar',
'core', 'core',
'content', 'content',
@ -61,6 +64,8 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
] ]
ROOT_URLCONF = 'freekake_api.urls' ROOT_URLCONF = 'freekake_api.urls'
@ -90,11 +95,11 @@ WSGI_APPLICATION = 'freekake_api.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'freekake_app_db', 'NAME': config('DB_NAME'),
'USER': 'postgres', 'USER': config('DB_USER'),
'PASSWORD': 'asalada123', 'PASSWORD': config('DB_PASSWORD'),
'HOST': 'localhost', 'HOST': config('DB_HOST', default='localhost'),
'PORT': '5432', 'PORT': config('DB_PORT', default='5432'),
} }
} }
@ -176,3 +181,12 @@ DJOSER = {
'SEND_ACTIVATION_EMAIL': True, 'SEND_ACTIVATION_EMAIL': True,
'SERIALIZERS': {}, 'SERIALIZERS': {},
} }
INTERNAL_IPS = [
'127.0.0.1',
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
DATA_UPLOAD_MAX_MEMORY_SIZE = 2 * 1024 * 1024 # 5MB

View File

@ -17,6 +17,9 @@ Including another URLconf
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from oauth2_provider import urls as oauth2_urls from oauth2_provider import urls as oauth2_urls
from debug_toolbar.toolbar import debug_toolbar_urls
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [ urlpatterns = [
# path('admin/', admin.site.urls), # path('admin/', admin.site.urls),
@ -27,4 +30,4 @@ urlpatterns = [
path('core/', include('core.urls')), path('core/', include('core.urls')),
path('content/', include('content.urls')), path('content/', include('content.urls')),
path('character/', include('character.urls')), path('character/', include('character.urls')),
] ]+ debug_toolbar_urls() + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)