From ed84b97f583f61ca88716d658eb695f1c3a2f726 Mon Sep 17 00:00:00 2001 From: Stepan Date: Sat, 27 Dec 2025 23:51:53 +0100 Subject: [PATCH] Publications and categories --- .gitignore | 3 +- project/settings/base.py | 1 + project/urls.py | 3 +- publications/__init__.py | 0 publications/admin.py | 19 +++ publications/apps.py | 6 + publications/managers.py | 26 ++++ publications/migrations/0001_initial.py | 43 ++++++ .../0002_rating_unique_user_publication.py | 19 +++ ...ation_user_rating_time_created_and_more.py | 33 +++++ .../migrations/0004_alter_publication_user.py | 21 +++ .../0005_category_publication_category.py | 26 ++++ publications/migrations/__init__.py | 0 publications/models.py | 96 +++++++++++++ publications/serializers.py | 36 +++++ publications/tests.py | 3 + publications/urls.py | 20 +++ publications/utils.py | 26 ++++ publications/views.py | 132 ++++++++++++++++++ ...id_school_index_alter_user_school_index.py | 24 ++++ users/models.py | 2 +- users/serializers.py | 15 +- users/urls.py | 4 +- users/views.py | 14 +- 24 files changed, 556 insertions(+), 16 deletions(-) create mode 100644 publications/__init__.py create mode 100644 publications/admin.py create mode 100644 publications/apps.py create mode 100644 publications/managers.py create mode 100644 publications/migrations/0001_initial.py create mode 100644 publications/migrations/0002_rating_unique_user_publication.py create mode 100644 publications/migrations/0003_publication_user_rating_time_created_and_more.py create mode 100644 publications/migrations/0004_alter_publication_user.py create mode 100644 publications/migrations/0005_category_publication_category.py create mode 100644 publications/migrations/__init__.py create mode 100644 publications/models.py create mode 100644 publications/serializers.py create mode 100644 publications/tests.py create mode 100644 publications/urls.py create mode 100644 publications/utils.py create mode 100644 publications/views.py create mode 100644 users/migrations/0004_alter_schoolid_school_index_alter_user_school_index.py diff --git a/.gitignore b/.gitignore index 595fb0c..26bbb60 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ db.sqlite3 .env __pycache__/ /static/ -/media/ \ No newline at end of file +/media/ +.venv/ \ No newline at end of file diff --git a/project/settings/base.py b/project/settings/base.py index 74a4d42..c5985fa 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -49,6 +49,7 @@ INSTALLED_APPS = [ 'django_filters', 'drf_spectacular', + 'publications.apps.PublicationsConfig', ] MIDDLEWARE = [ diff --git a/project/urls.py b/project/urls.py index 356bd9d..38eb4bd 100644 --- a/project/urls.py +++ b/project/urls.py @@ -24,6 +24,7 @@ urlpatterns = [ path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/', include([ - path('', include('users.urls')) + path('', include('users.urls')), + path('', include('publications.urls')) ])), ] diff --git a/publications/__init__.py b/publications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/publications/admin.py b/publications/admin.py new file mode 100644 index 0000000..8fb973b --- /dev/null +++ b/publications/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin +from .models import Publication, Rating + +# Register your models here. +class PublicationAdmin(admin.ModelAdmin): + list_display = ('pk', 'content_type', 'is_pinned', 'user', 'time_created', 'time_updated') + list_display_links = ('pk', 'content_type') + list_filter = ('content_type', 'is_pinned') + readonly_fields = ('time_created', 'time_updated') + ordering = ('-is_pinned', '-time_created') + search_fields = ('user__username', ) + +class RatingAdmin(admin.ModelAdmin): + list_display = ('publication', 'score', 'user', 'time_created') + readonly_fields = ('time_created', 'time_updated') + search_fields = ('user__username', ) + +admin.site.register(Publication, PublicationAdmin) +admin.site.register(Rating, RatingAdmin) \ No newline at end of file diff --git a/publications/apps.py b/publications/apps.py new file mode 100644 index 0000000..0081d99 --- /dev/null +++ b/publications/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PublicationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'publications' diff --git a/publications/managers.py b/publications/managers.py new file mode 100644 index 0000000..de85fa0 --- /dev/null +++ b/publications/managers.py @@ -0,0 +1,26 @@ +from typing import TypeVar +from django.db.models import Avg +from django.db import models + +T = TypeVar('T') + +class PublicationQuerySet(models.QuerySet[T]): + def with_score(self): + qs = self + qs = qs.annotate(average_score=Avg("ratings__score")) + return qs + + def only_publish(self): + from .models import Publication + qs = self + return qs.filter(status=Publication.StatusVarioations.PUBLIC.value) + +class PublicationManager(models.Manager[T]): + def get_queryset(self): + return PublicationQuerySet[T](self.model, using=self._db).with_score() + + def with_related(self): + return self.get_queryset().select_related('user', 'category') + + def only_publish(self): + return self.get_queryset().only_publish() \ No newline at end of file diff --git a/publications/migrations/0001_initial.py b/publications/migrations/0001_initial.py new file mode 100644 index 0000000..83babc6 --- /dev/null +++ b/publications/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.9 on 2025-12-27 20:07 + +import django.core.validators +import django.db.models.deletion +import publications.models +import publications.utils +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Publication', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(blank=True, null=True, upload_to=publications.models.user_directory_path, validators=[django.core.validators.FileExtensionValidator(['jpg', 'jpeg', 'png', 'webp', 'gif']), publications.utils.validate_image_size])), + ('video', models.FileField(blank=True, null=True, upload_to=publications.models.user_directory_path, validators=[django.core.validators.FileExtensionValidator(['mp4', 'webm', 'mov']), publications.utils.validate_video_size])), + ('content_type', models.CharField(choices=[('image', 'Image'), ('video', 'Video')], max_length=20)), + ('status', models.CharField(choices=[('public', 'Public'), ('pending', 'Pending')], default='pending', max_length=50)), + ('description', models.CharField(max_length=255)), + ('is_pinned', models.BooleanField(default=False)), + ('time_created', models.DateTimeField(auto_now_add=True)), + ('time_updated', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Rating', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('score', models.IntegerField(validators=[publications.utils.validate_score])), + ('publication', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='publications.publication')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/publications/migrations/0002_rating_unique_user_publication.py b/publications/migrations/0002_rating_unique_user_publication.py new file mode 100644 index 0000000..3ce87d5 --- /dev/null +++ b/publications/migrations/0002_rating_unique_user_publication.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.9 on 2025-12-27 20:09 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('publications', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddConstraint( + model_name='rating', + constraint=models.UniqueConstraint(fields=('user', 'publication'), name='unique_user_publication'), + ), + ] diff --git a/publications/migrations/0003_publication_user_rating_time_created_and_more.py b/publications/migrations/0003_publication_user_rating_time_created_and_more.py new file mode 100644 index 0000000..041ce4d --- /dev/null +++ b/publications/migrations/0003_publication_user_rating_time_created_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.9 on 2025-12-27 20:18 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('publications', '0002_rating_unique_user_publication'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='publication', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='rating', + name='time_created', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='rating', + name='time_updated', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/publications/migrations/0004_alter_publication_user.py b/publications/migrations/0004_alter_publication_user.py new file mode 100644 index 0000000..6d7aeef --- /dev/null +++ b/publications/migrations/0004_alter_publication_user.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.9 on 2025-12-27 20:18 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('publications', '0003_publication_user_rating_time_created_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='publication', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/publications/migrations/0005_category_publication_category.py b/publications/migrations/0005_category_publication_category.py new file mode 100644 index 0000000..57cb9b5 --- /dev/null +++ b/publications/migrations/0005_category_publication_category.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.9 on 2025-12-27 22:02 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('publications', '0004_alter_publication_user'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ], + ), + migrations.AddField( + model_name='publication', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='publications.category'), + ), + ] diff --git a/publications/migrations/__init__.py b/publications/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/publications/models.py b/publications/models.py new file mode 100644 index 0000000..28ff923 --- /dev/null +++ b/publications/models.py @@ -0,0 +1,96 @@ +import os +import uuid +from django.db import models +from django.core.validators import FileExtensionValidator +from django.core.exceptions import ValidationError + +from users.models import User +from .utils import validate_image_size, validate_video_size, validate_score +from .managers import PublicationManager + +ALLOWED_IMAGE_EXTS = ['jpg', 'jpeg', 'png', 'webp', 'gif'] +ALLOWED_VIDEO_EXTS = ['mp4', 'webm', 'mov'] + +def user_directory_path(instance, filename): + ext = os.path.splitext(filename)[1] + return f'publications/user_{instance.user.id}/{uuid.uuid4()}{ext}' + +class Category(models.Model): + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + +# Create your models here. +class Publication(models.Model): + class StatusVarioations(models.TextChoices): + PUBLIC = 'public', 'Public' + PENDING = 'pending', 'Pending' + + class ContentVariations(models.TextChoices): + IMAGE = 'image', 'Image' + VIDEO = 'video', 'Video' + + image = models.ImageField(upload_to=user_directory_path, blank=True, null=True, validators=[FileExtensionValidator(ALLOWED_IMAGE_EXTS), validate_image_size]) + video = models.FileField(upload_to=user_directory_path, blank=True, null=True, validators=[FileExtensionValidator(ALLOWED_VIDEO_EXTS), validate_video_size]) + content_type = models.CharField(max_length=20, choices=ContentVariations.choices) + status = models.CharField(max_length=50, choices=StatusVarioations.choices, default=StatusVarioations.PENDING) + description = models.CharField(max_length=255) + is_pinned = models.BooleanField(default=False) + user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False) + category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True) + + time_created = models.DateTimeField(auto_now_add=True) + time_updated = models.DateTimeField(auto_now=True) + + objects: PublicationManager["Publication"] = PublicationManager() + + def __str__(self): + return f'Content #{self.pk}' + + @property + def average_score(self) -> int: + return getattr(self, "average_score", None) + + def clean(self): + errors = [] + + if self.content_type == 'image' and not self.image: + errors.append('Required image file') + if self.content_type == 'video' and not self.video: + errors.append('Required video file') + + if self.image and self.video: + errors.append('You must upload either a video or an image, not both') + + if errors: + raise ValidationError(errors) + +class Rating(models.Model): + score = models.IntegerField(validators=[validate_score], null=False) + user = models.ForeignKey(User, on_delete=models.CASCADE) + publication = models.ForeignKey(Publication, on_delete=models.CASCADE, related_name="ratings") + + time_created = models.DateTimeField(auto_now_add=True) + time_updated = models.DateTimeField(auto_now=True) + + @staticmethod + def rate_publication(user: User, publication: Publication, score: int): + exists_rate = Rating.objects.filter(user=user, publication=publication).first() + if exists_rate: + exists_rate.score = score + exists_rate.save() + else: + Rating.objects.create( + user=user, + publication=publication, + score=score + ) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['user', 'publication'], name='unique_user_publication') + ] + + def __str__(self): + return f'Score {self.score} by {self.user} for {self.publication}' \ No newline at end of file diff --git a/publications/serializers.py b/publications/serializers.py new file mode 100644 index 0000000..3f28fbb --- /dev/null +++ b/publications/serializers.py @@ -0,0 +1,36 @@ +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from users.serializers import PublicUserSerializer +from .models import Publication, Rating, Category + +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = ('pk', 'name') + +class PublicationSerializer(serializers.ModelSerializer): + category_detail = CategorySerializer(source="category", read_only=True) + user_detail = PublicUserSerializer(source="user", read_only=True) + average_score = serializers.IntegerField(read_only=True) + + class Meta: + model = Publication + fields = ('pk', 'image', 'video', 'content_type', 'status', 'average_score', 'description', 'is_pinned', 'user', 'user_detail', 'category', 'category_detail','time_created', 'time_updated') + + read_only_fields = ('time_created', 'time_updated', 'is_pinned', 'user', 'status') + +class AdminPublicationSerializer(PublicationSerializer): + class Meta(PublicationSerializer.Meta): + read_only_fields = ('time_created', 'time_updated', 'user') + +class RatePublicationSerializer(serializers.Serializer): + score = serializers.IntegerField() + publication = serializers.PrimaryKeyRelatedField(queryset=Publication.objects.only_publish()) + + def validate_score(self, value): + if value > 5 or value < 1: + raise ValidationError('Score must be between 1 and 5') + + return value + \ No newline at end of file diff --git a/publications/tests.py b/publications/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/publications/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/publications/urls.py b/publications/urls.py new file mode 100644 index 0000000..e49225c --- /dev/null +++ b/publications/urls.py @@ -0,0 +1,20 @@ +from django.contrib import admin +from django.urls import path, include + +from .views import * + +urlpatterns = [ + path('publications/', include([ + path('', PublicationsAPIView.as_view()), + path('/', PublicationDetailAPIView.as_view()), + path('rate/', RatePublicationAPIView.as_view()) + ])), + path('admin/publications/', include([ + path('', AdminPublicationsAPIView.as_view()), + path('/', AdminPublicationDetailAPIView.as_view()) + ])), + path('categories/', include([ + path('', CategoryListAPIView.as_view()), + path('/', CategoryDetailAPIView.as_view()) + ])) +] diff --git a/publications/utils.py b/publications/utils.py new file mode 100644 index 0000000..7dd35cf --- /dev/null +++ b/publications/utils.py @@ -0,0 +1,26 @@ +from rest_framework.permissions import BasePermission +from django.core.exceptions import ValidationError + +from users.models import User + +MAX_IMAGE_SIZE_MB = 2 +MAX_VIDEO_SIZE_MB = 50 + +def validate_score(score): + if score < 1 or score > 5: + raise ValidationError('Score must be between 1 and 5') + +def validate_image_size(file): + max_size = MAX_IMAGE_SIZE_MB * 1024 * 1024 + if file.size > max_size: + raise ValidationError(f'Max size file is {MAX_IMAGE_SIZE_MB} MB') + +def validate_video_size(file): + max_size = MAX_VIDEO_SIZE_MB * 1024 * 1024 + if file.size > max_size: + raise ValidationError(f'Max size file is {MAX_VIDEO_SIZE_MB} MB') + +class IsProfessorOnly(BasePermission): + def has_permission(self, request, view): + user: User = request.user + return user.is_authenticated and (user.role == User.Roles.PROFESSOR.value or user.is_superuser) \ No newline at end of file diff --git a/publications/views.py b/publications/views.py new file mode 100644 index 0000000..99d1ed4 --- /dev/null +++ b/publications/views.py @@ -0,0 +1,132 @@ +from rest_framework.permissions import IsAuthenticated, IsAdminUser +from rest_framework.views import APIView +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView, RetrieveAPIView +from drf_spectacular.utils import extend_schema +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters +from rest_framework import status + +from .utils import IsProfessorOnly +from .models import Publication, Rating, Category +from .serializers import PublicationSerializer, AdminPublicationSerializer, RatePublicationSerializer, CategorySerializer +from users.models import User +from project.serializers import MessageResponseSerializer + +# Create your views here. +@extend_schema( + tags=['Publications'], + methods=['GET'], + summary='List publications', +) +@extend_schema( + tags=['Publications'], + methods=['POST'], + summary='Create new publication (only authorized)', +) +class PublicationsAPIView(ListCreateAPIView): + queryset = Publication.objects.with_related().only_publish() + serializer_class = PublicationSerializer + filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] + filterset_fields = ['is_pinned', 'user', 'content_type', 'category'] + ordering_fields = ['is_pinned', 'average_score', 'time_created', 'time_updated'] + ordering = ['-time_created'] + search_fields = ['user__username', 'description'] + + def get_permissions(self): + if self.request.method == 'POST': + return [IsAuthenticated()] + + return super().get_permissions() + +@extend_schema( + tags=['Publications'], + summary='Publication details', +) +class PublicationDetailAPIView(RetrieveAPIView): + queryset = Publication.objects.with_related().only_publish() + serializer_class = PublicationSerializer + +@extend_schema( + tags=['Publications'], + methods=['GET'], + summary='List publications (only admin or professor)', +) +@extend_schema( + tags=['Publications'], + methods=['POST'], + summary='Create new publication (only admin or professor)', +) +class AdminPublicationsAPIView(PublicationsAPIView): + queryset = Publication.objects.with_related() + serializer_class = AdminPublicationSerializer + filterset_fields = ['is_pinned', 'user', 'content_type', 'category', 'status'] + ordering_fields = ['is_pinned', 'average_score', 'time_created', 'time_updated', 'status'] + + def get_permissions(self): + return [IsProfessorOnly()] + +@extend_schema( + tags=['Publications'], + summary='CRUD for publication (only admin or professor)', +) +class AdminPublicationDetailAPIView(RetrieveUpdateDestroyAPIView): + queryset = Publication.objects.with_related() + serializer_class = AdminPublicationSerializer + permission_classes = [IsProfessorOnly] + +@extend_schema( + tags=['Categories'], + methods=['GET'], + summary='List categories', +) +@extend_schema( + tags=['Categories'], + methods=['POST'], + summary='Create new category (only admin)', +) +class CategoryListAPIView(ListCreateAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + ordering = ['name'] + search_fields = ['name'] + + def get_permissions(self): + if self.request.method == 'POST': + return [IsAdminUser] + return super().get_permissions() + +@extend_schema( + tags=['Categories'], + summary='CRUD for category (only admin)', +) +class CategoryDetailAPIView(RetrieveUpdateDestroyAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer + permission_classes = [IsAdminUser] + +class RatePublicationAPIView(APIView): + permission_classes = [IsAuthenticated] + + @extend_schema( + tags=['Publications'], + summary='Rate publication', + request=RatePublicationSerializer, + responses=MessageResponseSerializer + ) + def post(self, request: Request, format=None): + serializer = RatePublicationSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + user: User = request.user + publication = serializer.validated_data['publication'] + score = serializer.validated_data['score'] + + Rating.rate_publication(user, publication, score) + + return Response( + {"detail": "Rating saved successfully"}, + status=status.HTTP_200_OK + ) \ No newline at end of file diff --git a/users/migrations/0004_alter_schoolid_school_index_alter_user_school_index.py b/users/migrations/0004_alter_schoolid_school_index_alter_user_school_index.py new file mode 100644 index 0000000..ef9c4b9 --- /dev/null +++ b/users/migrations/0004_alter_schoolid_school_index_alter_user_school_index.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.9 on 2025-12-27 20:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0003_schoolid_remove_user_school_id_user_school_index'), + ] + + operations = [ + migrations.AlterField( + model_name='schoolid', + name='school_index', + field=models.CharField(db_index=True, max_length=8, unique=True), + ), + migrations.AlterField( + model_name='user', + name='school_index', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='users.schoolid'), + ), + ] diff --git a/users/models.py b/users/models.py index 9952c33..adbb587 100644 --- a/users/models.py +++ b/users/models.py @@ -16,7 +16,7 @@ class User(AbstractUser): PROFESSOR = 'prof', 'Professor' school_index = models.ForeignKey(SchoolID, on_delete=models.PROTECT) - role = models.CharField(max_length=20, choices=Roles, default=Roles.COMMON) + role = models.CharField(max_length=20, choices=Roles.choices, default=Roles.COMMON) @staticmethod def authenticate(request, username_or_schoolid, password): diff --git a/users/serializers.py b/users/serializers.py index 395b7ab..2a02516 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -6,13 +6,20 @@ class SchoolIDSerializer(serializers.ModelSerializer): model = SchoolID fields = ('id', 'school_index') -class UserSerializer(serializers.ModelSerializer): - is_superuser = serializers.BooleanField(read_only=True) - school_index_object = SchoolIDSerializer(source='school_index', read_only=True) +class PublicUserSerializer(serializers.ModelSerializer): + school_index_detail = SchoolIDSerializer(source='school_index', read_only=True) class Meta: model = User - fields = ('id', 'username', 'school_index_object', 'school_index', 'email', 'role', 'is_superuser') + fields = ('id', 'username', 'school_index_detail', 'school_index', 'role', 'is_superuser') + +class UserSerializer(serializers.ModelSerializer): + is_superuser = serializers.BooleanField(read_only=True) + school_index_detail = SchoolIDSerializer(source='school_index', read_only=True) + + class Meta: + model = User + fields = ('id', 'username', 'school_index_detail', 'school_index', 'email', 'role', 'is_superuser') class UserForAdminSerializer(UserSerializer): is_superuser = serializers.BooleanField() diff --git a/users/urls.py b/users/urls.py index eb4cbe7..4989806 100644 --- a/users/urls.py +++ b/users/urls.py @@ -10,12 +10,12 @@ urlpatterns = [ path('register/', RegisterView.as_view()), ])), - path('users/', include([ + path('admin/users/', include([ path('/', UserAPIView.as_view()), path('', UserListAPIView.as_view()) ])), - path('schools/', include([ + path('admin/schools/', include([ path('/', SchoolAPIView.as_view()), path('', SchoolListAPIView.as_view()) ])), diff --git a/users/views.py b/users/views.py index 63bc3b4..3b1b9fa 100644 --- a/users/views.py +++ b/users/views.py @@ -19,7 +19,7 @@ from .serializers import UserSerializer, LoginSerializer, RegisterSerializer, To @extend_schema(tags=['Auth'], - description="Get current authenticated user") + summary="Get current authenticated user") class AboutMeView(RetrieveAPIView): serializer_class = UserSerializer @@ -31,7 +31,7 @@ class AboutMeView(RetrieveAPIView): class LoginView(APIView): @extend_schema(tags=['Auth'], - description='Authenticate using login or password', + summary='Authenticate using login or password', request=LoginSerializer, responses={ 200: TokenSerializer, @@ -54,7 +54,7 @@ class LoginView(APIView): class RegisterView(APIView): @extend_schema(tags=['Auth'], - description='Register new user using school_id', + summary='Register new user using school_id', request=RegisterSerializer, responses={ 201: MessageResponseSerializer, @@ -74,28 +74,28 @@ class RegisterView(APIView): @extend_schema(tags=['Users'], - description='List of all current users') + summary='List of all current users (only admin)') class UserListAPIView(ListAPIView): queryset = User.objects.all() serializer_class = UserForAdminSerializer permission_classes = [IsAdminUser] @extend_schema(tags=['Users'], - description='CRUD for specific user') + summary='CRUD for specific user (only admin)') class UserAPIView(RetrieveUpdateDestroyAPIView): queryset = User.objects.all() serializer_class = UserForAdminSerializer permission_classes = [IsAdminUser] @extend_schema(tags=['Users'], - description='List of all available school ids') + summary='List of all available school ids (only admin)') class SchoolListAPIView(ListAPIView): queryset = SchoolID.objects.all() serializer_class = SchoolIDSerializer permission_classes = [IsAdminUser] @extend_schema(tags=['Users'], - description='CRUD for specific school id') + summary='CRUD for specific school id (only admin)') class SchoolAPIView(RetrieveUpdateDestroyAPIView): queryset = SchoolID.objects.all() serializer_class = SchoolIDSerializer