Skip to content

Commit

Permalink
Upload files (#906)
Browse files Browse the repository at this point in the history
* Create files app to manage user uploaded files

Signed-off-by: Tmpecho <[email protected]>

* created serializers for file and gallery

* Move file upload endpoint from content/ to files/, create file and gallery views, add file and gallery model methods

Signed-off-by: Tmpecho <[email protected]>

* updating progress, needed to rename 'Gallery' to 'UserGallery' as a Gallery model already exists

* finished mvp for file project

* updating progress

* mvp for file uploading/deletion

* forgot changelog, oopsie

* fixed error in permissions from allowing non-admins to delete files, fixed 'security threat'

* Minor fixes to file serializer

Signed-off-by: Tmpecho <[email protected]>

* Fix whitespace

Signed-off-by: Tmpecho <[email protected]>

* Add custom exeption with mixins for files class

Signed-off-by: Tmpecho <[email protected]>

* Update custom file exceptions, set max gallery size as global constant, throw custom exceptions

Signed-off-by: Tmpecho <[email protected]>

* create test for creating file when not having a gallery, format

* removed duplicate method

* small refactoring and removed 'url' from file model, changed it to 'OptionalFile'

* fix lint

---------

Signed-off-by: Tmpecho <[email protected]>
Co-authored-by: Tmpecho <[email protected]>
Co-authored-by: Mads Nylund <[email protected]>
Co-authored-by: Johannes Aamot-Skeidsvoll <[email protected]>
  • Loading branch information
4 people authored Nov 4, 2024
1 parent a2185c6 commit 246ad2a
Show file tree
Hide file tree
Showing 45 changed files with 909 additions and 24 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
- 🎨**Overordnet**. Endret variabel og funksjonsnavn til å følge konvensjoner og andre små endringer.
-**Filtrering**. Admin kan nå filtere deltakere av et arrangement på studie, studieår, om deltakere har allergier, (om deltakere godtar å bli tatt bilde av, om deltakere har ankommet), i tillegg til søk på fornavn og etternavn.

-**Filopplasting**. Det er nå mulig for admin brukere å laste opp- og slette filer.
-**Mail endepunkt**. Det er nå laget et endepunkt for å sende mailer.

## Versjon 2024.10.11
-**Tilbakemelding-funksjon**. Man kan nå opprette tilbakemeldinger for bugs og idé.
- 🦟 **Påmelding**. Det vil nå ikke være mulig med flere påmeldinger på et arrangement enn maksgrensen.
Expand Down
2 changes: 2 additions & 0 deletions app/common/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ class BaseModelSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
if hasattr(instance, "image") and "image" in validated_data:
replace_file(instance.image, validated_data.get("image", None))
if hasattr(instance, "file") and "file" in validated_data:
replace_file(instance.file, validated_data.get("file", None))
return super().update(instance, validated_data)
6 changes: 4 additions & 2 deletions app/communication/models/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,7 @@ def send(self, connection):
return is_success

def __str__(self):
return (f"\"{self.subject}\", to {self.users.all()[0] if self.users.count() == 1 else f'{self.users.count()} users'}, "
f"{'sent' if self.sent else 'eta'} {self.eta}")
return (
f"\"{self.subject}\", to {self.users.all()[0] if self.users.count() == 1 else f'{self.users.count()} users'}, "
f"{'sent' if self.sent else 'eta'} {self.eta}"
)
2 changes: 2 additions & 0 deletions app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@
SLACK_BEDPRES_OG_KURS_CHANNEL_ID = "C01DCSJ8X2Q"
SLACK_ARRANGEMENTER_CHANNEL_ID = "C01LFEFJFV3"

MAX_GALLERY_SIZE = 50

# TODO: Create api-urls as constants which then can be used in for example tests and urls.py files
4 changes: 0 additions & 4 deletions app/content/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@
UserCalendarEvents,
UserViewSet,
accept_form,
delete,
register_with_feide,
send_email,
upload,
)

router = routers.DefaultRouter()
Expand Down Expand Up @@ -53,9 +51,7 @@
urlpatterns = [
re_path(r"", include(router.urls)),
path("accept-form/", accept_form),
path("upload/", upload),
path("send-email/", send_email),
path("delete-file/<str:container_name>/<str:blob_name>/", delete),
path("feide/", register_with_feide),
re_path(r"users/(?P<user_id>[^/.]+)/events.ics", UserCalendarEvents()),
]
2 changes: 1 addition & 1 deletion app/content/util/event_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from app.content.exceptions import RefundFailedError
from app.payment.tasks import check_if_has_paid
from app.payment.util.payment_utils import (
check_access_token,
initiate_payment,
refund_payment,
check_access_token
)


Expand Down
1 change: 0 additions & 1 deletion app/content/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from app.content.views.news import NewsViewSet
from app.content.views.page import PageViewSet
from app.content.views.short_link import ShortLinkViewSet
from app.content.views.upload import upload, delete
from app.content.views.strike import StrikeViewSet
from app.content.views.toddel import ToddelViewSet
from app.content.views.qr_code import QRCodeViewSet
Expand Down
8 changes: 5 additions & 3 deletions app/content/views/accept_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ def accept_form(request):
if len(types) == 1 and types[0].lower() == "annonse"
else MAIL_NOK_LEADER
)
title = (f"{body['info']['bedrift']} vil ha {', '.join(types[:-1])}"
f"{' og ' if len(types) > 1 else ''}{', '.join(types[-1:])}, "
f"{', '.join(times[:-1])}{' og ' if len(times) > 1 else ''}{', '.join(times[-1:])}")
title = (
f"{body['info']['bedrift']} vil ha {', '.join(types[:-1])}"
f"{' og ' if len(types) > 1 else ''}{', '.join(types[-1:])}, "
f"{', '.join(times[:-1])}{' og ' if len(times) > 1 else ''}{', '.join(times[-1:])}"
)

is_success = send_html_email(
to_mails=[to_mail],
Expand Down
4 changes: 1 addition & 3 deletions app/content/views/user_bio.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,4 @@ def update(self, request, *args, **kwargs):

def destroy(self, request, *args, **kwargs):
super().destroy(request, *args, **kwargs)
return Response(
{"detail": "Brukerbio ble slettet"}, status=status.HTTP_200_OK
)
return Response({"detail": "Brukerbio ble slettet"}, status=status.HTTP_200_OK)
2 changes: 1 addition & 1 deletion app/content/views/user_calendar_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def __call__(self, request, *args, **kwargs):
return JsonResponse(
{
"detail": "Denne brukeren har skrudd av offentlig deling av påmeldinger til arrangementer. "
"Du kan derfor ikke hente ut brukerens arrangementer som .ics-fil"
"Du kan derfor ikke hente ut brukerens arrangementer som .ics-fil"
},
status=status.HTTP_403_FORBIDDEN,
)
Expand Down
Empty file added app/files/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions app/files/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.contrib import admin

from app.files import models

admin.site.register(models.UserGallery)
admin.site.register(models.File)
5 changes: 5 additions & 0 deletions app/files/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class FilesConfig(AppConfig):
name = "app.files"
Empty file added app/files/enums.py
Empty file.
22 changes: 22 additions & 0 deletions app/files/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from rest_framework import status
from rest_framework.exceptions import APIException, ValidationError

from app.constants import MAX_GALLERY_SIZE


class APINoGalleryFoundForUser(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "Ingen galleri ble funnet for brukeren."


class NoGalleryFoundForUser(ValidationError):
default_detail = "Ingen galleri ble funnet for brukeren."


class APIGalleryIsFull(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = f"Galleriet er fullt med {MAX_GALLERY_SIZE} filer."


class GalleryIsFull(ValidationError):
default_detail = f"Galleriet er fullt med {MAX_GALLERY_SIZE} filer."
Empty file added app/files/factories/__init__.py
Empty file.
Empty file added app/files/filters/__init__.py
Empty file.
68 changes: 68 additions & 0 deletions app/files/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Generated by Django 4.2.16 on 2024-10-07 16:45

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="UserGallery",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"author",
models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="user_galleries",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="File",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("title", models.CharField(max_length=80)),
("url", models.URLField()),
("description", models.TextField(blank=True)),
(
"gallery",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="files",
to="files.usergallery",
),
),
],
),
]
22 changes: 22 additions & 0 deletions app/files/migrations/0002_remove_file_url_file_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 5.1.1 on 2024-10-31 15:22

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("files", "0001_initial"),
]

operations = [
migrations.RemoveField(
model_name="file",
name="url",
),
migrations.AddField(
model_name="file",
name="file",
field=models.URLField(blank=True, max_length=600, null=True),
),
]
Empty file.
17 changes: 17 additions & 0 deletions app/files/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from app.files.exceptions import (
APIGalleryIsFull,
APINoGalleryFoundForUser,
GalleryIsFull,
NoGalleryFoundForUser,
)
from app.util.mixins import APIErrorsMixin


class FileErrorMixin(APIErrorsMixin):
@property
def expected_exceptions(self):
return {
**super().expected_exceptions,
NoGalleryFoundForUser: APINoGalleryFoundForUser,
GalleryIsFull: APIGalleryIsFull,
}
2 changes: 2 additions & 0 deletions app/files/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from app.files.models.user_gallery import UserGallery
from app.files.models.file import File
68 changes: 68 additions & 0 deletions app/files/models/file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from django.db import models
from django.db.models import PROTECT

from app.common.enums import AdminGroup, Groups
from app.common.permissions import BasePermissionModel, check_has_access
from app.files.models.user_gallery import UserGallery
from app.util.models import BaseModel, OptionalFile


class File(BaseModel, BasePermissionModel, OptionalFile):
read_access = AdminGroup.admin()
write_access = AdminGroup.admin()

title = models.CharField(max_length=80)

description = models.TextField(blank=True)
gallery = models.ForeignKey(
UserGallery, on_delete=PROTECT, related_name="files", blank=False
)

class Meta:
pass

def __str__(self):
return self.title

@classmethod
def has_read_permission(cls, request):
return super().has_read_permission(request)

@classmethod
def has_write_permission(cls, request):
return check_has_access(Groups.TIHLDE, request)

@classmethod
def has_retrieve_permission(cls, request):
return cls.has_read_permission(request)

@classmethod
def has_create_permission(cls, request):
return check_has_access(cls.write_access, request)

@classmethod
def has_update_permission(cls, request):
return check_has_access(cls.write_access, request)

@classmethod
def has_destroy_permission(cls, request):
return check_has_access(Groups.TIHLDE, request)

@classmethod
def has_list_permission(cls, request):
return cls.has_read_permission(request)

def has_object_read_permission(self, request):
return self.has_read_permission(request)

def has_object_write_permission(self, request):
return self.has_write_permission(request)

def has_object_retrieve_permission(self, request):
return self.has_object_read_permission(request)

def has_object_update_permission(self, request):
return self.gallery.author == request.user

def has_object_destroy_permission(self, request):
return self.gallery.author == request.user
Loading

0 comments on commit 246ad2a

Please sign in to comment.