Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event registration serialization #386

Merged
merged 8 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dj-rest-auth = "~=5.0.1"
django = "~=3.2.23"
django-allauth = "~=0.58.2"
django-filter = "~=23.5"
django-sendfile2 = "~=0.7.1"
djangorestframework = "~=3.14.0"
drf-writable-nested = "~=0.7.0"
pillow = "~=10.3.0"
Expand Down
37 changes: 33 additions & 4 deletions competition/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
from personal.models import Profile, School
from user.models import User

private_storage = FileSystemStorage(location=settings.PRIVATE_STORAGE_ROOT)
private_storage = FileSystemStorage(location=settings.PRIVATE_STORAGE_ROOT,
base_url='/protected/'
)


class CompetitionType(models.Model):
Expand Down Expand Up @@ -619,6 +621,14 @@ class Vote(models.IntegerChoices):
POSITIVE = 1, 'pozitívny'


def get_solution_path(instance, filename): #pylint: disable=unused-argument
return instance.get_solution_file_path()


def get_corrected_solution_path(instance, filename): #pylint: disable=unused-argument
return instance.get_corrected_solution_file_path()


class Solution(models.Model):
"""
Popisuje riešenie úlohy od užívateľa. Obsahuje nahraté aj opravné riešenie, body
Expand All @@ -634,11 +644,11 @@ class Meta:
solution = RestrictedFileField(
content_types=['application/pdf'],
storage=private_storage,
verbose_name='účastnícke riešenie', blank=True, upload_to='solutions/user_solutions')
verbose_name='účastnícke riešenie', blank=True, upload_to=get_solution_path)
corrected_solution = RestrictedFileField(
content_types=['application/pdf'],
storage=private_storage,
verbose_name='opravené riešenie', blank=True, upload_to='solutions/corrected/')
verbose_name='opravené riešenie', blank=True, upload_to=get_corrected_solution_path)

score = models.PositiveSmallIntegerField(
verbose_name='body', null=True, blank=True)
Expand All @@ -664,13 +674,32 @@ def get_solution_file_name(self):
return f'{self.semester_registration.profile.user.get_full_name_camel_case()}'\
f'-{self.problem.id}-{self.semester_registration.id}.pdf'

def get_solution_file_path(self):
return f'solutions/user_solutions/{self.get_solution_file_name()}'

def get_corrected_solution_file_name(self):
return f'corrected/{self.semester_registration.profile.user.get_full_name_camel_case()}'\
return f'{self.semester_registration.profile.user.get_full_name_camel_case()}'\
f'-{self.problem.id}-{self.semester_registration.id}_corrected.pdf'

def get_corrected_solution_file_path(self):
return f'solutions/corrected/{self.get_corrected_solution_file_name()}'

def can_user_modify(self, user):
return self.problem.can_user_modify(user)

def can_access(self, user):
return self.semester_registration.profile.user == user or self.can_user_modify(user)

@classmethod
def get_by_filepath(cls, path):
try:
return cls.objects.get(solution=path)
except cls.DoesNotExist:
try:
return cls.objects.get(corrected_solution=path)
except cls.DoesNotExist:
return None

@classmethod
def can_user_create(cls, user: User, data: dict) -> bool:
problem = Problem.objects.get(pk=data['problem'])
Expand Down
6 changes: 5 additions & 1 deletion competition/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,15 @@ def get_history_events(self, obj):
class EventRegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = models.EventRegistration
fields = ['school', 'grade', 'profile']
fields = ['school', 'grade', 'profile', 'verbose_name', 'id', 'event']
school = SchoolShortSerializer(many=False)
grade = serializers.SlugRelatedField(
slug_field='tag', many=False, read_only=True)
profile = ProfileShortSerializer(many=False)
verbose_name = serializers.SerializerMethodField('get_verbose_name')

def get_verbose_name(self, obj):
return str(obj)


@ts_interface(context='competition')
Expand Down
13 changes: 7 additions & 6 deletions competition/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,11 @@ def upload_solution(self, request, pk=None):
problem=problem,
semester_registration=event_registration,
late_tag=late_tag,
is_online=True
is_online=True,
solution=file
)
solution.solution.save(
solution.get_solution_file_name(), file, save=True)
# solution.solution.save(
# solution.get_solution_file_name(), file, save=True)

return Response(status=status.HTTP_201_CREATED)

Expand Down Expand Up @@ -394,7 +395,7 @@ def upload_solutions_with_points(self, request, pk=None):
except (IndexError, ValueError, AssertionError):
errors.append({
'filename': filename,
'status': 'Nedá sa prečítať názov súboru. Skontroluj, že názov súboru'
'status': 'Nedá sa prečítať názov súboru. Skontroluj, že názov súboru'
'je v tvare BODY-MENO-ID_ULOHY-ID_REGISTRACIE_USERA.pdf'
})
continue
Expand Down Expand Up @@ -583,7 +584,7 @@ def upload_solution_file(self, request, pk=None):
raise exceptions.ParseError(
detail='Riešenie nie je vo formáte pdf')
solution.solution.save(
solution.get_solution_file_name(), file, save=True
solution.get_solution_file_path(), file, save=True
)
return Response(status=status.HTTP_201_CREATED)

Expand All @@ -600,7 +601,7 @@ def upload_corrected_solution_file(self, request, pk=None):
raise exceptions.ParseError(
detail='Riešenie nie je vo formáte pdf')
solution.corrected_solution.save(
solution.get_corrected_solution_file_name(), file, save=True
solution.get_corrected_solution_file_path(), file, save=True
)
return Response(status=status.HTTP_201_CREATED)

Expand Down
Empty file added downloads/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions downloads/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class DownloadsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'downloads'
Empty file.
3 changes: 3 additions & 0 deletions downloads/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.db import models

# Create your models here.
3 changes: 3 additions & 0 deletions downloads/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
18 changes: 18 additions & 0 deletions downloads/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.conf.urls import url
from django.utils.translation import ugettext_lazy as _

from competition.models import Solution

from .views import download_protected_file

urlpatterns = [
# Include non-translated versions only since Admin ignores lang prefix
url(r'solutions/(?P<path>.*)$', download_protected_file,
{'path_prefix': 'solutions/', 'model_class': Solution},
name='download_solution'),
url(r'corrected_solutions/(?P<path>.*)$', download_protected_file,
{'path_prefix': 'corrected_solutions/',
'model_class': Solution},
name='download_corrected_solution'),

]
29 changes: 29 additions & 0 deletions downloads/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os

from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_sendfile import sendfile
from rest_framework.request import Request


@login_required
def download_protected_file(request: Request, model_class, path_prefix, path):
"""
This view allows download of the file at the specified path, if the user
is allowed to. This is checked by calling the model's can_access_files
method.
"""
filepath = os.path.join(settings.PRIVATE_STORAGE_ROOT, path_prefix, path)
filepath_mediapath = path_prefix + path

if request.user.is_authenticated:
# Superusers can access all files
if request.user.is_superuser:
return sendfile(request, filepath)
obj = model_class.get_by_filepath(filepath_mediapath)

if obj is not None and obj.can_access(request.user):
return sendfile(request, filepath)

raise PermissionDenied
2 changes: 1 addition & 1 deletion personal/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def grade(self, value):
pk=value).get_year_of_graduation_by_date()

def __str__(self):
return str(self.user)
return f'{self.full_name()} ({self.user})'

def full_name(self):
return f'{self.first_name} {self.last_name}'
22 changes: 19 additions & 3 deletions personal/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,20 @@ class Meta:

@ts_interface(context='personal')
class SchoolSerializer(serializers.ModelSerializer):
verbose_name = serializers.SerializerMethodField('get_verbose_name')
id = serializers.SerializerMethodField('get_id')

class Meta:
model = School
fields = '__all__'
fields = ['id', 'code', 'name', 'abbreviation', 'street',
'city', 'zip_code', 'email', 'district', 'verbose_name']
read_only_fields = ['id', 'verbose_name']

def get_verbose_name(self, obj):
return str(obj)

def get_id(self, obj):
return obj.code


@ts_interface(context='personal')
Expand Down Expand Up @@ -55,13 +66,15 @@ class ProfileSerializer(serializers.ModelSerializer):
has_school = serializers.SerializerMethodField('get_has_school')
school_id = serializers.IntegerField()
email = serializers.EmailField(source='user.email')
verbose_name = serializers.SerializerMethodField('get_verbose_name')

class Meta:
model = Profile
fields = ['grade_name', 'id', 'email', 'first_name', 'last_name', 'school',
'phone', 'parent_phone', 'grade', 'is_student', 'has_school', 'school_id']
'phone', 'parent_phone', 'grade', 'is_student', 'has_school',
'school_id', 'verbose_name']
read_only_fields = ['grade_name', 'id', 'first_name', 'last_name',
'email', 'is_student', 'has_school', 'school'] # 'year_of_graduation',
'email', 'is_student', 'has_school', 'school', 'verbose_name']

extra_kwargs = {
'grade': {
Expand All @@ -72,6 +85,9 @@ class Meta:
}
}

def get_verbose_name(self, obj):
return str(obj)

def get_is_student(self, obj):
return obj.school != School.objects.get(pk=1)

Expand Down
2 changes: 1 addition & 1 deletion personal/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class SchoolViewSet(viewsets.ModelViewSet):
serializer_class = SchoolSerializer
filterset_fields = ['district', 'district__county']
filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['name', 'street']
search_fields = ['name', 'street', 'city']

def destroy(self, request, *args, **kwargs):
"""Zmazanie školy"""
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Django==3.2.25
django-allauth==0.58.2
django-filter==23.5
django-rest-swagger==2.2.0
django-sendfile2==0.7.1
django-typomatic==2.5.0
djangorestframework==3.14.0
drf-writable-nested==0.7.0
Expand Down
5 changes: 3 additions & 2 deletions webstrom/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,9 @@
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

MEDIA_URL = '/media/'
PRIVATE_STORAGE_ROOT = os.path.join(BASE_DIR, 'protected_media')

PRIVATE_STORAGE_ROOT = os.path.join(BASE_DIR, 'protected_media/')
SENDFILE_ROOT = PRIVATE_STORAGE_ROOT
SENDFILE_BACKEND = "django_sendfile.backends.simple"
# Email backend

EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
Expand Down
1 change: 1 addition & 0 deletions webstrom/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
path('cms/', include('cms.urls')),
path('personal/', include('personal.urls')),
path('base/', include('base.urls')),
path('protected/', include('downloads.urls')),
# Dočasná cesta pre allauth bez rest frameworku
path('accounts/', include('allauth.urls')),
]
Expand Down
Loading