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
|
.env
|
||||||
__pycache__/
|
__pycache__/
|
||||||
/static/
|
/static/
|
||||||
/media/
|
/media/
|
||||||
|
.venv/
|
||||||
@ -49,6 +49,7 @@ INSTALLED_APPS = [
|
|||||||
'django_filters',
|
'django_filters',
|
||||||
'drf_spectacular',
|
'drf_spectacular',
|
||||||
|
|
||||||
|
'publications.apps.PublicationsConfig',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
@ -24,6 +24,7 @@ urlpatterns = [
|
|||||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||||
path('api/', include([
|
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'
|
PROFESSOR = 'prof', 'Professor'
|
||||||
|
|
||||||
school_index = models.ForeignKey(SchoolID, on_delete=models.PROTECT)
|
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
|
@staticmethod
|
||||||
def authenticate(request, username_or_schoolid, password):
|
def authenticate(request, username_or_schoolid, password):
|
||||||
|
|||||||
@ -6,13 +6,20 @@ class SchoolIDSerializer(serializers.ModelSerializer):
|
|||||||
model = SchoolID
|
model = SchoolID
|
||||||
fields = ('id', 'school_index')
|
fields = ('id', 'school_index')
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class PublicUserSerializer(serializers.ModelSerializer):
|
||||||
is_superuser = serializers.BooleanField(read_only=True)
|
school_index_detail = SchoolIDSerializer(source='school_index', read_only=True)
|
||||||
school_index_object = SchoolIDSerializer(source='school_index', read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
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):
|
class UserForAdminSerializer(UserSerializer):
|
||||||
is_superuser = serializers.BooleanField()
|
is_superuser = serializers.BooleanField()
|
||||||
|
|||||||
@ -10,12 +10,12 @@ urlpatterns = [
|
|||||||
path('register/', RegisterView.as_view()),
|
path('register/', RegisterView.as_view()),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
path('users/', include([
|
path('admin/users/', include([
|
||||||
path('<int:pk>/', UserAPIView.as_view()),
|
path('<int:pk>/', UserAPIView.as_view()),
|
||||||
path('', UserListAPIView.as_view())
|
path('', UserListAPIView.as_view())
|
||||||
])),
|
])),
|
||||||
|
|
||||||
path('schools/', include([
|
path('admin/schools/', include([
|
||||||
path('<int:pk>/', SchoolAPIView.as_view()),
|
path('<int:pk>/', SchoolAPIView.as_view()),
|
||||||
path('', SchoolListAPIView.as_view())
|
path('', SchoolListAPIView.as_view())
|
||||||
])),
|
])),
|
||||||
|
|||||||
@ -19,7 +19,7 @@ from .serializers import UserSerializer, LoginSerializer, RegisterSerializer, To
|
|||||||
|
|
||||||
|
|
||||||
@extend_schema(tags=['Auth'],
|
@extend_schema(tags=['Auth'],
|
||||||
description="Get current authenticated user")
|
summary="Get current authenticated user")
|
||||||
class AboutMeView(RetrieveAPIView):
|
class AboutMeView(RetrieveAPIView):
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ class AboutMeView(RetrieveAPIView):
|
|||||||
|
|
||||||
class LoginView(APIView):
|
class LoginView(APIView):
|
||||||
@extend_schema(tags=['Auth'],
|
@extend_schema(tags=['Auth'],
|
||||||
description='Authenticate using login or password',
|
summary='Authenticate using login or password',
|
||||||
request=LoginSerializer,
|
request=LoginSerializer,
|
||||||
responses={
|
responses={
|
||||||
200: TokenSerializer,
|
200: TokenSerializer,
|
||||||
@ -54,7 +54,7 @@ class LoginView(APIView):
|
|||||||
|
|
||||||
class RegisterView(APIView):
|
class RegisterView(APIView):
|
||||||
@extend_schema(tags=['Auth'],
|
@extend_schema(tags=['Auth'],
|
||||||
description='Register new user using school_id',
|
summary='Register new user using school_id',
|
||||||
request=RegisterSerializer,
|
request=RegisterSerializer,
|
||||||
responses={
|
responses={
|
||||||
201: MessageResponseSerializer,
|
201: MessageResponseSerializer,
|
||||||
@ -74,28 +74,28 @@ class RegisterView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
@extend_schema(tags=['Users'],
|
@extend_schema(tags=['Users'],
|
||||||
description='List of all current users')
|
summary='List of all current users (only admin)')
|
||||||
class UserListAPIView(ListAPIView):
|
class UserListAPIView(ListAPIView):
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = UserForAdminSerializer
|
serializer_class = UserForAdminSerializer
|
||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
@extend_schema(tags=['Users'],
|
@extend_schema(tags=['Users'],
|
||||||
description='CRUD for specific user')
|
summary='CRUD for specific user (only admin)')
|
||||||
class UserAPIView(RetrieveUpdateDestroyAPIView):
|
class UserAPIView(RetrieveUpdateDestroyAPIView):
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = UserForAdminSerializer
|
serializer_class = UserForAdminSerializer
|
||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
@extend_schema(tags=['Users'],
|
@extend_schema(tags=['Users'],
|
||||||
description='List of all available school ids')
|
summary='List of all available school ids (only admin)')
|
||||||
class SchoolListAPIView(ListAPIView):
|
class SchoolListAPIView(ListAPIView):
|
||||||
queryset = SchoolID.objects.all()
|
queryset = SchoolID.objects.all()
|
||||||
serializer_class = SchoolIDSerializer
|
serializer_class = SchoolIDSerializer
|
||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
@extend_schema(tags=['Users'],
|
@extend_schema(tags=['Users'],
|
||||||
description='CRUD for specific school id')
|
summary='CRUD for specific school id (only admin)')
|
||||||
class SchoolAPIView(RetrieveUpdateDestroyAPIView):
|
class SchoolAPIView(RetrieveUpdateDestroyAPIView):
|
||||||
queryset = SchoolID.objects.all()
|
queryset = SchoolID.objects.all()
|
||||||
serializer_class = SchoolIDSerializer
|
serializer_class = SchoolIDSerializer
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user