diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 91ea44a..44983ba 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -1,3 +1,14 @@ -from django.shortcuts import render - -# Create your views here. +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +class MeView(APIView): + permission_classes = [IsAuthenticated] + + def get(self,request): + return Response({ + "id": request.user.id, + "username": request.user.username, + "email": request.user.email, + "is_staff": request.user.is_staff, + }) \ No newline at end of file diff --git a/apps/common/api/auto_router.py b/apps/common/api/auto_router.py new file mode 100644 index 0000000..8bb28aa --- /dev/null +++ b/apps/common/api/auto_router.py @@ -0,0 +1,9 @@ +def auto_register(router, viewsets: dict): + """ + viewsets = { + "articles": ArticleViewSet, + "courses": CourseViewSet, + } + """ + for prefix, viewset in viewsets.items(): + router.register(prefix, viewset, basename=prefix) \ No newline at end of file diff --git a/apps/common/api/base_serializer.py b/apps/common/api/base_serializer.py new file mode 100644 index 0000000..34ee10c --- /dev/null +++ b/apps/common/api/base_serializer.py @@ -0,0 +1,11 @@ +from rest_framework.serializers import ModelSerializer + +class BaseModelSerializer(ModelSerializer): + """ + Base Serializer สำหรับ CMS‑like API + - ใช้ fields = "__all__" เป็น default + """ + + class Meta: + abstract = True + fields = "__all__" \ No newline at end of file diff --git a/apps/common/api/base_viewset.py b/apps/common/api/base_viewset.py new file mode 100644 index 0000000..f93d06d --- /dev/null +++ b/apps/common/api/base_viewset.py @@ -0,0 +1,29 @@ +from rest_framework.viewsets import ModelViewSet +from rest_framework.permissions import IsAuthenticated +from rest_framework.filters import SearchFilter, OrderingFilter +from django_filters.rest_framework import DjangoFilterBackend + +class BaseModelViewSet(ModelViewSet): + """ + CMS-like Base ViewSet + - CRUD อัตโนมัติ + - Pagination (จาก settings.py) + - Filter / Search / Ordering + - Permission กลาง + """ + + # default permission (override ได้) + permission_classes = [IsAuthenticated] + + # filter backend มาตรฐานแบบ CMS + filter_backends = [ + DjangoFilterBackend, + SearchFilter, + OrderingFilter, + ] + + # ค่า default (override ต่อ model) + filterset_fields = "__all__" + search_fields = [] + ordering_fields = "__all__" + ordering = ["-id"] \ No newline at end of file diff --git a/apps/common/api/permissions.py b/apps/common/api/permissions.py new file mode 100644 index 0000000..ed50c6a --- /dev/null +++ b/apps/common/api/permissions.py @@ -0,0 +1,12 @@ +from rest_framework.permissions import BasePermission, SAFE_METHODS + +class IsStaffOrReadOnly(BasePermission): + """ + - อ่านได้ทุกคน + - เขียนได้เฉพาะ staff + """ + + def has_permission(self, request, view): + if request.method in SAFE_METHODS: + return True + return request.user and request.user.is_staff \ No newline at end of file diff --git a/apps/common/urls.py b/apps/common/urls.py index ea92e29..339cdaf 100644 --- a/apps/common/urls.py +++ b/apps/common/urls.py @@ -1,7 +1,38 @@ -from django.urls import path - -from apps.common.views import HealthCheckView - -urlpatterns = [ - path("health/", HealthCheckView.as_view(), name="health-check"), +from django.urls import path, include +from rest_framework.routers import DefaultRouter + +from apps.common.api.auto_router import auto_register +from apps.common.views import HealthCheckView +from apps.content.views import ArticleViewSet +from apps.courses.views import CourseViewSet + +# Action-based Views (Enrollment / Progress) +from apps.courses.views import ( + EnrollCourseView, + MyCoursesView, + CompleteLessonView, +) + +# Router สำหรับ CMS-like API +router = DefaultRouter() + +# ใช้ auto_register เฉพาะกับ CMS‑like resources +auto_register(router, { + "articles" : ArticleViewSet, + "courses" : CourseViewSet, +}) + +urlpatterns = [ + # Health check + path("health/", HealthCheckView.as_view(), name="health-check"), + + # CMS-like API + path("api/", include(router.urls)), + + # Enrollment (user ↔ course) + path("api/courses//enroll/", EnrollCourseView.as_view(), name="enroll-course"), + path("api/my/courses/", MyCoursesView.as_view(), name="my-courses"), + + # Progress tracking (user ↔ lesson) + path("api/lessons//complete/", CompleteLessonView.as_view(), name="complete-lesson"), ] \ No newline at end of file diff --git a/apps/content/admin.py b/apps/content/admin.py index 8c38f3f..a2e0fb0 100644 --- a/apps/content/admin.py +++ b/apps/content/admin.py @@ -1,3 +1,10 @@ -from django.contrib import admin - -# Register your models here. +from django.contrib import admin +from apps.content.models import Article +from unfold.admin import ModelAdmin + +@admin.register(Article) +class ArticleAdmin(ModelAdmin): + list_display = ('id', 'title', 'published', 'created_at') + list_filter = ('published',) + search_fields = ('title',) + diff --git a/apps/content/apps.py b/apps/content/apps.py index 73d42e5..bdcbbbe 100644 --- a/apps/content/apps.py +++ b/apps/content/apps.py @@ -1,5 +1,5 @@ -from django.apps import AppConfig - - -class ContentConfig(AppConfig): - name = 'apps.content' +from django.apps import AppConfig + +class ContentConfig(AppConfig): + name = 'apps.content' + verbose_name = 'คลังเนื้อหา' diff --git a/apps/content/migrations/0001_initial.py b/apps/content/migrations/0001_initial.py new file mode 100644 index 0000000..29439c6 --- /dev/null +++ b/apps/content/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.2 on 2026-05-02 23:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Article', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('body', models.TextField()), + ('published', models.BooleanField(default=False)), + ('create_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/apps/content/migrations/0002_rename_create_at_article_created_at.py b/apps/content/migrations/0002_rename_create_at_article_created_at.py new file mode 100644 index 0000000..b534dca --- /dev/null +++ b/apps/content/migrations/0002_rename_create_at_article_created_at.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.2 on 2026-05-02 23:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='article', + old_name='create_at', + new_name='created_at', + ), + ] diff --git a/apps/content/migrations/0003_alter_article_options.py b/apps/content/migrations/0003_alter_article_options.py new file mode 100644 index 0000000..3d45de6 --- /dev/null +++ b/apps/content/migrations/0003_alter_article_options.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.2 on 2026-05-03 02:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0002_rename_create_at_article_created_at'), + ] + + operations = [ + migrations.AlterModelOptions( + name='article', + options={'verbose_name': 'บทความ', 'verbose_name_plural': 'บทความ'}, + ), + ] diff --git a/apps/content/models.py b/apps/content/models.py index 71a8362..29512c5 100644 --- a/apps/content/models.py +++ b/apps/content/models.py @@ -1,3 +1,14 @@ -from django.db import models - -# Create your models here. +from django.db import models + +class Article(models.Model): + title = models.CharField(max_length=200) # หัวข้อบทความ + body = models.TextField() # เนื้อหา + published = models.BooleanField(default=False) # ใช้เปิด/ปิดการแสดงผล + created_at = models.DateTimeField(auto_now_add=True) # เวลาสร้าง (เอาไว้ sort / paginate) + + class Meta: + verbose_name = "บทความ" + verbose_name_plural = "บทความ" + + def __str__(self): + return self.title diff --git a/apps/content/serializers.py b/apps/content/serializers.py new file mode 100644 index 0000000..ed284bd --- /dev/null +++ b/apps/content/serializers.py @@ -0,0 +1,6 @@ +from apps.common.api.base_serializer import BaseModelSerializer +from apps.content.models import Article + +class ArticleSerializer(BaseModelSerializer): + class Meta(BaseModelSerializer.Meta): + model = Article \ No newline at end of file diff --git a/apps/content/views.py b/apps/content/views.py index 91ea44a..1aed612 100644 --- a/apps/content/views.py +++ b/apps/content/views.py @@ -1,3 +1,12 @@ -from django.shortcuts import render - -# Create your views here. +from apps.common.api.base_viewset import BaseModelViewSet +from apps.content.models import Article +from apps.content.serializers import ArticleSerializer + +class ArticleViewSet(BaseModelViewSet): + queryset = Article.objects.all() + serializer_class = ArticleSerializer + + # CMS-like behavior + search_fields = ('title', 'body') + filterset_fields = ['published'] + ordering = ['-created_at'] \ No newline at end of file diff --git a/apps/courses/admin.py b/apps/courses/admin.py index 8c38f3f..ecc8c28 100644 --- a/apps/courses/admin.py +++ b/apps/courses/admin.py @@ -1,3 +1,29 @@ -from django.contrib import admin - -# Register your models here. +from django.contrib import admin +from unfold.admin import TabularInline, ModelAdmin +from apps.courses.models import Lesson, Course, Enrollment, LessonProgress + + +class LessonInLine(TabularInline): + model = Lesson + extra = 1 + +@admin.register(Course) +class CourseAdmin(ModelAdmin): + inlines = [LessonInLine] + list_display = ('id', 'title', 'published', 'created_at') + list_filter = ('published',) + +@admin.register(Lesson) +class LessonAdmin(ModelAdmin): + list_display = ('id', 'title', 'course', 'order') + list_filter = ('course',) + +@admin.register(Enrollment) +class EnrollmentAdmin(ModelAdmin): + list_display = ('id', 'course', 'enrolled_at') + list_filter = ('course',) + +@admin.register(LessonProgress) +class LessonProgressAdmin(ModelAdmin): + list_display = ('user', 'lesson', 'completed') + list_filter = ('lesson', 'completed') \ No newline at end of file diff --git a/apps/courses/apps.py b/apps/courses/apps.py index e81cadf..87924b7 100644 --- a/apps/courses/apps.py +++ b/apps/courses/apps.py @@ -1,5 +1,6 @@ -from django.apps import AppConfig - - -class CoursesConfig(AppConfig): - name = 'apps.courses' +from django.apps import AppConfig + + +class CoursesConfig(AppConfig): + name = 'apps.courses' + verbose_name = 'หลักสูตร' diff --git a/apps/courses/migrations/0001_initial.py b/apps/courses/migrations/0001_initial.py new file mode 100644 index 0000000..fae8f11 --- /dev/null +++ b/apps/courses/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.2 on 2026-05-03 00:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Course', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField()), + ('published', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Lesson', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('content', models.TextField()), + ('order', models.PositiveIntegerField(default=0)), + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='courses.course')), + ], + ), + ] diff --git a/apps/courses/migrations/0002_enrollment_lessonprogress.py b/apps/courses/migrations/0002_enrollment_lessonprogress.py new file mode 100644 index 0000000..d218565 --- /dev/null +++ b/apps/courses/migrations/0002_enrollment_lessonprogress.py @@ -0,0 +1,41 @@ +# Generated by Django 6.0.2 on 2026-05-03 01:32 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Enrollment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('enrolled_at', models.DateTimeField(auto_now_add=True)), + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to='courses.course')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'course')}, + }, + ), + migrations.CreateModel( + name='LessonProgress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('completed', models.BooleanField(default=False)), + ('complete_at', models.DateTimeField(auto_now_add=True)), + ('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress_records', to='courses.lesson')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lesson_progress', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'lesson')}, + }, + ), + ] diff --git a/apps/courses/migrations/0003_alter_course_options_alter_enrollment_options_and_more.py b/apps/courses/migrations/0003_alter_course_options_alter_enrollment_options_and_more.py new file mode 100644 index 0000000..5e431f4 --- /dev/null +++ b/apps/courses/migrations/0003_alter_course_options_alter_enrollment_options_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 6.0.2 on 2026-05-03 02:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0002_enrollment_lessonprogress'), + ] + + operations = [ + migrations.AlterModelOptions( + name='course', + options={'verbose_name': 'หลักสูตร', 'verbose_name_plural': 'หลักสูตร'}, + ), + migrations.AlterModelOptions( + name='enrollment', + options={'verbose_name': 'การลงทะเบียนเรียน', 'verbose_name_plural': 'การลงทะเบียนเรียน'}, + ), + migrations.AlterModelOptions( + name='lesson', + options={'verbose_name': 'บทเรียน', 'verbose_name_plural': 'บทเรียน'}, + ), + migrations.AlterModelOptions( + name='lessonprogress', + options={'verbose_name': 'ความคืบหน้าบทเรียน', 'verbose_name_plural': 'ความคืบหน้าบทเรียน'}, + ), + ] diff --git a/apps/courses/models.py b/apps/courses/models.py index 71a8362..7608210 100644 --- a/apps/courses/models.py +++ b/apps/courses/models.py @@ -1,3 +1,73 @@ -from django.db import models - -# Create your models here. +from django.db import models +from core import settings + +User = settings.AUTH_USER_MODEL + +class Enrollment(models.Model): + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='enrollments', + ) + course = models.ForeignKey( + "Course", + on_delete=models.CASCADE, + related_name='enrollments', + ) + enrolled_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = (('user', 'course'),) + verbose_name = "การลงทะเบียนเรียน" + verbose_name_plural = "การลงทะเบียนเรียน" + + def __str__(self): + return f"{self.user} enrolled in {self.course}" + +class LessonProgress(models.Model): + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='lesson_progress', + ) + lesson = models.ForeignKey( + "Lesson", + on_delete=models.CASCADE, + related_name='progress_records', + ) + completed = models.BooleanField(default=False) + complete_at = models.DateTimeField(auto_now_add=True) + class Meta: + unique_together = (('user', 'lesson'),) + verbose_name = "ความคืบหน้าบทเรียน" + verbose_name_plural = "ความคืบหน้าบทเรียน" + +class Course(models.Model): + title = models.CharField(max_length=200) + description = models.TextField() + published = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = "หลักสูตร" + verbose_name_plural = "หลักสูตร" + + def __str__(self): + return self.title + +class Lesson(models.Model): + course = models.ForeignKey( + Course, + related_name='lessons', + on_delete=models.CASCADE + ) + title = models.CharField(max_length=200) + content = models.TextField() + order = models.PositiveIntegerField(default=0) + + class Meta: + verbose_name = "บทเรียน" + verbose_name_plural = "บทเรียน" + + def __str__(self): + return f"{self.course.title} - {self.title}" \ No newline at end of file diff --git a/apps/courses/serializers.py b/apps/courses/serializers.py new file mode 100644 index 0000000..a4c7d60 --- /dev/null +++ b/apps/courses/serializers.py @@ -0,0 +1,28 @@ +from rest_framework import serializers + +from apps.common.api.base_serializer import BaseModelSerializer +from apps.courses.models import Lesson, Course + +class LessonSerializer(BaseModelSerializer): + class Meta(BaseModelSerializer.Meta): + model = Lesson + +class CourseSerializer(BaseModelSerializer): + lessons = LessonSerializer(many=True, read_only=True) + + class Meta(BaseModelSerializer.Meta): + model = Course + +class EnrollCourseResponseSerializer(serializers.Serializer): + course_id = serializers.IntegerField() + enrolled_at = serializers.DateTimeField() + +class MyCourseSerializer(serializers.Serializer): + course_id = serializers.IntegerField() + title = serializers.CharField() + enrolled_at = serializers.DateTimeField() + +class CompleteLessonResponseSerializer(serializers.Serializer): + lesson_id = serializers.IntegerField() + completed = serializers.BooleanField() + completed_at = serializers.DateTimeField() \ No newline at end of file diff --git a/apps/courses/views.py b/apps/courses/views.py index 91ea44a..c7b5c13 100644 --- a/apps/courses/views.py +++ b/apps/courses/views.py @@ -1,3 +1,73 @@ -from django.shortcuts import render - -# Create your views here. +from django.utils import timezone +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from apps.common.api.base_viewset import BaseModelViewSet +from apps.common.api.permissions import IsStaffOrReadOnly +from apps.courses.models import Course, Enrollment, Lesson, LessonProgress +from apps.courses.serializers import CourseSerializer, EnrollCourseResponseSerializer, MyCourseSerializer, \ + CompleteLessonResponseSerializer + + +class CourseViewSet(BaseModelViewSet): + queryset = Course.objects.all() + serializer_class = CourseSerializer + permission_classes = [IsStaffOrReadOnly] + + search_fields = ('title', 'description') + filterset_fields = ['published'] + ordering = ['-created_at'] + +class EnrollCourseView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, course_id): + course = Course.objects.get(id=course_id) + enrollment, created = Enrollment.objects.get_or_create(user=request.user, course=course) + serializer = EnrollCourseResponseSerializer({ + 'course_id' : course.id, + 'enrolled_at' : enrollment.enrolled_at, + }) + + return Response(serializer.data) + +class MyCoursesView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + enrollments = Enrollment.objects.filter(user=request.user) + + data = [ + { + 'course_id': e.course.id, + 'title': e.course.title, + 'enrolled_at': e.enrolled_at, + } for e in enrollments + ] + + serializer = MyCourseSerializer(data, many=True) + return Response(serializer.data) + +class CompleteLessonView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, lesson_id): + lesson = Lesson.objects.get(id=lesson_id) + + progress, _ = LessonProgress.objects.get_or_create( + user=request.user, + lesson=lesson + ) + + progress.completed = True + progress.complete_at = timezone.now() + progress.save() + + serializer = CompleteLessonResponseSerializer({ + 'lesson_id' : lesson.id, + 'completed' : progress.completed, + 'completed_at' : progress.complete_at, + }) + + return Response(serializer.data) \ No newline at end of file diff --git a/core/settings.py b/core/settings.py index ad957a0..67411e7 100644 --- a/core/settings.py +++ b/core/settings.py @@ -10,6 +10,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/6.0/ref/settings/ """ import os +from datetime import timedelta from pathlib import Path from django.templatetags.static import static @@ -48,6 +49,7 @@ INSTALLED_APPS = [ # Third-party Apps 'rest_framework', # สำหรับจัดการ API 'corsheaders', # สำหรับจัดการ CORS (สำคัญมากถ้ามีหน้าบ้านแยก) + "django_filters", # LOCAL APPS 'apps.accounts', @@ -109,9 +111,34 @@ DATABASES = { REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( + # สำหรับ Django Admin (ยังจำเป็น) "rest_framework.authentication.SessionAuthentication", - "rest_framework.authentication.TokenAuthentication", + # สำหรับ API ทั้งหมด + "rest_framework_simplejwt.authentication.JWTAuthentication", ), + + + "DEFAULT_PERMISSION_CLASSES": ( + # ค่า default ต้อง login ก่อน + "rest_framework.permissions.IsAuthenticated", + ), + + + "DEFAULT_FILTER_BACKENDS": [ + "django_filters.rest_framework.DjangoFilterBackend", + ], + + "DEFAULT_PAGINATION_CLASS": ( + "rest_framework.pagination.PageNumberPagination" + ), + "PAGE_SIZE": 10, + +} + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15), + "REFRESH_TOKEN_LIFETIME": timedelta(days=7), + "AUTH_HEADER_TYPES": ("Bearer",), # กำหนด client ส่ง header แบบ Authorization: Bearer } CORS_ALLOW_ALL_ORIGINS = True # ควรเป็น False ใน Production diff --git a/core/urls.py b/core/urls.py index 2b17726..5c10042 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,23 +1,19 @@ -""" -URL configuration for core project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/6.0/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path, include - -urlpatterns = [ - path('admin/', admin.site.urls), - path('', include('apps.common.urls')), -] +from django.contrib import admin +from django.urls import path, include +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView + +from apps.accounts.views import MeView + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('apps.common.urls')), + + # Login + path('api/auth/login/',TokenObtainPairView.as_view(), name='token_obtain_pair'), + + # Refresh token + path('api/auth/refresh/',TokenRefreshView.as_view(), name='token_refresh'), + + # Me + path('api/auth/me/', MeView.as_view(), name='me'), +] diff --git a/requirements.txt b/requirements.txt index 6fc1b26..ad14788 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,11 @@ -Django==6.0.2 -djangorestframework==3.17.1 -django-unfold==0.90.0 -djangorestframework-simplejwt==5.5.1 -django-allauth==65.16.1 -python-dotenv==1.2.2 -psycopg[binary]==3.3.3 -django-cors-headers==4.9.0 -gunicorn==25.3.0 -whitenoise==6.12.0 \ No newline at end of file +Django==6.0.2 +djangorestframework==3.17.1 +django-unfold==0.90.0 +djangorestframework-simplejwt==5.5.1 +django-allauth==65.16.1 +python-dotenv==1.2.2 +psycopg[binary]==3.3.3 +django-cors-headers==4.9.0 +gunicorn==25.3.0 +whitenoise==6.12.0 +django-filter==25.2 \ No newline at end of file