Publications and categories
This commit is contained in:
parent
df896cee39
commit
ed84b97f58
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,4 +2,5 @@ db.sqlite3
|
||||
.env
|
||||
__pycache__/
|
||||
/static/
|
||||
/media/
|
||||
/media/
|
||||
.venv/
|
||||
@ -49,6 +49,7 @@ INSTALLED_APPS = [
|
||||
'django_filters',
|
||||
'drf_spectacular',
|
||||
|
||||
'publications.apps.PublicationsConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@ -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'))
|
||||
])),
|
||||
]
|
||||
|
||||
0
publications/__init__.py
Normal file
0
publications/__init__.py
Normal file
19
publications/admin.py
Normal file
19
publications/admin.py
Normal file
@ -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)
|
||||
6
publications/apps.py
Normal file
6
publications/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PublicationsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'publications'
|
||||
26
publications/managers.py
Normal file
26
publications/managers.py
Normal file
@ -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()
|
||||
43
publications/migrations/0001_initial.py
Normal file
43
publications/migrations/0001_initial.py
Normal file
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
21
publications/migrations/0004_alter_publication_user.py
Normal file
21
publications/migrations/0004_alter_publication_user.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
0
publications/migrations/__init__.py
Normal file
0
publications/migrations/__init__.py
Normal file
96
publications/models.py
Normal file
96
publications/models.py
Normal file
@ -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}'
|
||||
36
publications/serializers.py
Normal file
36
publications/serializers.py
Normal file
@ -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
|
||||
|
||||
3
publications/tests.py
Normal file
3
publications/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
20
publications/urls.py
Normal file
20
publications/urls.py
Normal file
@ -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('<int:pk>/', PublicationDetailAPIView.as_view()),
|
||||
path('rate/', RatePublicationAPIView.as_view())
|
||||
])),
|
||||
path('admin/publications/', include([
|
||||
path('', AdminPublicationsAPIView.as_view()),
|
||||
path('<int:pk>/', AdminPublicationDetailAPIView.as_view())
|
||||
])),
|
||||
path('categories/', include([
|
||||
path('', CategoryListAPIView.as_view()),
|
||||
path('<int:pk>/', CategoryDetailAPIView.as_view())
|
||||
]))
|
||||
]
|
||||
26
publications/utils.py
Normal file
26
publications/utils.py
Normal file
@ -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)
|
||||
132
publications/views.py
Normal file
132
publications/views.py
Normal file
@ -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
|
||||
)
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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):
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -10,12 +10,12 @@ urlpatterns = [
|
||||
path('register/', RegisterView.as_view()),
|
||||
])),
|
||||
|
||||
path('users/', include([
|
||||
path('admin/users/', include([
|
||||
path('<int:pk>/', UserAPIView.as_view()),
|
||||
path('', UserListAPIView.as_view())
|
||||
])),
|
||||
|
||||
path('schools/', include([
|
||||
path('admin/schools/', include([
|
||||
path('<int:pk>/', SchoolAPIView.as_view()),
|
||||
path('', SchoolListAPIView.as_view())
|
||||
])),
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user