Skip to content

Commit 211b51f

Browse files
committed
feat: ApplicationFileAttachmentViewSet
1 parent 0b97b17 commit 211b51f

File tree

3 files changed

+55
-17
lines changed

3 files changed

+55
-17
lines changed

backend/samfundet/models/recruitment.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from django.contrib.auth.models import UserManager
1313

1414
from root.utils.mixins import CustomBaseModel, FullCleanSaveMixin
15-
from samfundet.utils import upload_to_app
15+
from samfundet.utils import upload_to_application_filepath
1616

1717
from .general import Gang, User, Campus, GangSection, Organization
1818
from .model_choices import RecruitmentStatusChoices, RecruitmentApplicantStates, RecruitmentPriorityChoices
@@ -516,10 +516,11 @@ class ApplicationFileAttachment(CustomBaseModel):
516516
application = models.ForeignKey(
517517
RecruitmentApplication, on_delete=models.CASCADE, related_name='attachments', help_text='The recruitment application this file is attached to'
518518
)
519-
application_file = models.FileField(upload_to='recruitment/application_attachments/')
519+
application_file = models.FileField(upload_to=upload_to_application_filepath)
520520
application_file_type = models.CharField(max_length=50, blank=True)
521521

522522
def clean(self) -> None:
523+
# FIX: this should probably be set by the recruitment position
523524
super().clean()
524525
if self.application_file:
525526
file_type = self.application_file.content_type
@@ -532,11 +533,16 @@ def clean(self) -> None:
532533
]
533534
if file_type not in allowed_types:
534535
raise ValidationError('Wrong filetype')
535-
if self.file.size > 10 * 1024 * 1024: # 10MB limit
536+
if self.application_file.size > 10 * 1024 * 1024: # 10MB limit
536537
raise ValidationError('File size must be less than 10MB.')
537538

538539
def __str__(self):
539-
return f'Attachment for {self.application} - {self.file.name}'
540+
return f'Attachment for {self.application} - {self.application_file.name}'
541+
542+
def delete(self, *args, **kwargs) -> None:
543+
storage, path = self.application_file.storage, self.application_file.path
544+
super().delete(*args, **kwargs)
545+
storage.delete(path)
540546

541547

542548
class RecruitmentInterviewAvailability(CustomBaseModel):

backend/samfundet/serializers.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from django.contrib.auth import authenticate
1515
from django.core.exceptions import ValidationError
1616
from django.core.files.images import ImageFile
17+
from django.core.files.uploadedfile import UploadedFile
1718
from django.contrib.auth.models import Group, Permission
1819
from django.contrib.auth.password_validation import validate_password
1920

@@ -77,15 +78,20 @@
7778
class ApplicationFileAttachmentSerializer(CustomBaseSerializer):
7879
class Meta:
7980
model = ApplicationFileAttachment
80-
fields = (
81-
'id',
82-
'application',
83-
'application_file',
84-
'application_file_type',
85-
)
81+
fields = ['id', 'application', 'application_file', 'application_file_type', 'created_at']
82+
read_only_fields = ['id', 'application_file_type', 'created_at']
8683

87-
def validate(self, attrs: dict) -> dict:
88-
return attrs
84+
def validate_application_file(self, value: UploadedFile) -> UploadedFile:
85+
if not value:
86+
raise serializers.ValidationError('A file must be provided.')
87+
return value
88+
89+
def create(self, validated_data: dict[str, Any]) -> ApplicationFileAttachment:
90+
application = validated_data.get('application') or self.context.get('application')
91+
if not application:
92+
raise serializers.ValidationError('Application is required to attach a file.')
93+
validated_data['application'] = application
94+
return super().create(validated_data)
8995

9096

9197
class TagSerializer(CustomBaseSerializer):

backend/samfundet/views.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
from .utils import generate_timeslots, get_occupied_timeslots_from_request
4040
from .serializers import (
41+
ApplicationFileAttachmentSerializer,
4142
InterviewSerializer,
4243
RecruitmentSerializer,
4344
InterviewRoomSerializer,
@@ -121,13 +122,38 @@ def verify_signature(*, payload_body: Any, secret_token: str, signature_header:
121122

122123

123124
class ApplicationFileAttachmentViewSet(ModelViewSet):
124-
serializer_class = ApplicationFileAttachmentSerializer
125125
queryset = ApplicationFileAttachment.objects.all()
126+
serializer_class = ApplicationFileAttachmentSerializer
127+
permission_classes = [IsAuthenticated]
128+
parser_classes = (MultiPartParser, FormParser)
126129

127-
# Typically, you might want extra permission logic here:
128-
# - Only the applicant or a recruiter with certain perms can read attachments
129-
# - Only the applicant can create attachments for *their own* application
130-
# etc.
130+
def get_queryset(self) -> Response | None:
131+
# Restrict to the user's applications or recruiter permissions
132+
# FIX: Consider permissions
133+
user = self.request.user
134+
if user.is_staff:
135+
return self.queryset
136+
return self.queryset.filter(application__user=user)
137+
138+
def create(self, request: Request) -> Response | None:
139+
application_id = request.data.get('application_id')
140+
try:
141+
application = RecruitmentApplication.objects.get(id=application_id, user=request.user)
142+
except RecruitmentApplication.DoesNotExist:
143+
return Response({'error': 'Application not found or not authorized'}, status=status.HTTP_404_NOT_FOUND)
144+
145+
serializer = self.get_serializer(data=request.data, context={'application': application})
146+
serializer.is_valid(raise_exception=True)
147+
self.perform_create(serializer)
148+
return Response(serializer.data, status=status.HTTP_201_CREATED)
149+
150+
def destroy(self, request: Request) -> Response | None:
151+
instance = self.get_object()
152+
# FIX: Consider permissions
153+
if instance.application.user != request.user and not request.user.is_staff:
154+
return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN)
155+
self.perform_destroy(instance)
156+
return Response(status=status.HTTP_204_NO_CONTENT)
131157

132158

133159
@method_decorator(ensure_csrf_cookie, 'dispatch')

0 commit comments

Comments
 (0)