Merge pull request #1053 from ae-utbm/sas-upload

Improve SAS upload
This commit is contained in:
thomas girod 2025-04-09 19:32:55 +02:00 committed by GitHub
commit 60fd72917d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 314 additions and 296 deletions

View File

@ -740,8 +740,9 @@ Welcome to the wiki page!
size=file.size,
)
pict.file.name = p.name
pict.clean()
pict.full_clean()
pict.generate_thumbnails()
pict.save()
img_skia = Picture.objects.get(name="skia.jpg")
img_sli = Picture.objects.get(name="sli.jpg")

View File

@ -1,16 +1,29 @@
from pathlib import Path
from typing import Annotated
from typing import Annotated, Any
from annotated_types import MinLen
from django.contrib.staticfiles.storage import staticfiles_storage
from django.db.models import Q
from django.urls import reverse
from django.utils.text import slugify
from django.utils.translation import gettext as _
from haystack.query import SearchQuerySet
from ninja import FilterSchema, ModelSchema, Schema
from ninja import FilterSchema, ModelSchema, Schema, UploadedFile
from pydantic import AliasChoices, Field
from pydantic_core.core_schema import ValidationInfo
from core.models import Group, SithFile, User
from core.utils import is_image
class UploadedImage(UploadedFile):
@classmethod
def _validate(cls, v: Any, info: ValidationInfo) -> Any:
super()._validate(v, info)
if not is_image(v):
msg = _("This file is not a valid image")
raise ValueError(msg)
return v
class SimpleUserSchema(ModelSchema):

View File

@ -17,15 +17,30 @@ from datetime import date, timedelta
# Image utils
from io import BytesIO
from typing import Final
import PIL
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import UploadedFile
from django.http import HttpRequest
from django.utils.timezone import localdate
from PIL import ExifTags
from PIL.Image import Image, Resampling
RED_PIXEL_PNG: Final[bytes] = (
b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52"
b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53"
b"\xde\x00\x00\x00\x0c\x49\x44\x41\x54\x08\xd7\x63\xf8\xcf\xc0\x00"
b"\x00\x03\x01\x01\x00\x18\xdd\x8d\xb0\x00\x00\x00\x00\x49\x45\x4e"
b"\x44\xae\x42\x60\x82"
)
"""A single red pixel, in PNG format.
Can be used in tests and in dev, when there is a need
to generate a dummy image that is considered valid nonetheless
"""
def get_start_of_semester(today: date | None = None) -> date:
"""Return the date of the start of the semester of the given date.
@ -97,6 +112,18 @@ def get_semester_code(d: date | None = None) -> str:
return "P" + str(start.year)[-2:]
def is_image(file: UploadedFile):
try:
im = PIL.Image.open(file.file)
im.verify()
# go back to the start of the file, without closing it.
# Otherwise, further checks on django side will fail
file.seek(0)
except PIL.UnidentifiedImageError:
return False
return True
def resize_image(
im: Image, edge: int, img_format: str, *, optimize: bool = True
) -> ContentFile:

View File

@ -32,17 +32,10 @@ from django.utils import timezone
from club.models import Club, Membership
from core.models import Group, Page, SithFile, User
from core.utils import RED_PIXEL_PNG
from sas.models import Album, PeoplePictureRelation, Picture
from subscription.models import Subscription
RED_PIXEL_PNG: Final[bytes] = (
b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52"
b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53"
b"\xde\x00\x00\x00\x0c\x49\x44\x41\x54\x08\xd7\x63\xf8\xcf\xc0\x00"
b"\x00\x03\x01\x01\x00\x18\xdd\x8d\xb0\x00\x00\x00\x00\x49\x45\x4e"
b"\x44\xae\x42\x60\x82"
)
USER_PACK_SIZE: Final[int] = 1000

View File

@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-06 15:54+0200\n"
"POT-Creation-Date: 2025-04-08 16:20+0200\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@ -116,7 +116,7 @@ msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur"
msgid "You should specify a role"
msgstr "Vous devez choisir un rôle"
#: club/forms.py sas/views.py
#: club/forms.py sas/forms.py
msgid "You do not have the permission to do that"
msgstr "Vous n'avez pas la permission de faire cela"
@ -1047,7 +1047,7 @@ msgid "Posters - edit"
msgstr "Affiche - modifier"
#: com/templates/com/poster_list.jinja com/templates/com/screen_list.jinja
#: sas/templates/sas/main.jinja
#: sas/templates/sas/fragments/album_create_form.jinja
msgid "Create"
msgstr "Créer"
@ -1644,6 +1644,10 @@ msgstr "étiquette"
msgid "operation type"
msgstr "type d'opération"
#: core/schemas.py
msgid "This file is not a valid image"
msgstr "Ce fichier n'est pas une image valide"
#: core/templates/core/403.jinja
msgid "403, Forbidden"
msgstr "403, Non autorisé"
@ -2729,7 +2733,7 @@ msgstr "Ajouter un nouveau dossier"
msgid "Error creating folder %(folder_name)s: %(msg)s"
msgstr "Erreur de création du dossier %(folder_name)s : %(msg)s"
#: core/views/files.py core/views/forms.py sas/forms.py
#: core/views/files.py core/views/forms.py
#, python-format
msgid "Error uploading file %(file_name)s: %(msg)s"
msgstr "Erreur d'envoi du fichier %(file_name)s : %(msg)s"
@ -4715,11 +4719,6 @@ msgstr "Ajouter un nouvel album"
msgid "Upload images"
msgstr "Envoyer les images"
#: sas/forms.py
#, python-format
msgid "Error creating album %(album)s: %(msg)s"
msgstr "Erreur de création de l'album %(album)s : %(msg)s"
#: sas/forms.py
msgid "You already requested moderation for this picture."
msgstr "Vous avez déjà déposé une demande de retrait pour cette photo."

View File

@ -1,7 +1,10 @@
from typing import Any, Literal
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import F
from django.urls import reverse
from ninja import Query
from ninja import Body, File, Query
from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.exceptions import NotFound, PermissionDenied
from ninja_extra.pagination import PageNumberPaginationExtra
@ -9,8 +12,15 @@ from ninja_extra.permissions import IsAuthenticated
from ninja_extra.schemas import PaginatedResponseSchema
from pydantic import NonNegativeInt
from core.auth.api_permissions import CanAccessLookup, CanView, IsInGroup, IsRoot
from core.auth.api_permissions import (
CanAccessLookup,
CanEdit,
CanView,
IsInGroup,
IsRoot,
)
from core.models import Notification, User
from core.schemas import UploadedImage
from sas.models import Album, PeoplePictureRelation, Picture
from sas.schemas import (
AlbumAutocompleteSchema,
@ -92,6 +102,38 @@ class PicturesController(ControllerBase):
.annotate(album=F("parent__name"))
)
@route.post(
"",
permissions=[CanEdit],
response={
200: None,
409: dict[Literal["detail"], dict[str, list[str]]],
422: dict[Literal["detail"], list[dict[str, Any]]],
},
url_name="upload_picture",
)
def upload_picture(self, album_id: Body[int], picture: File[UploadedImage]):
album = self.get_object_or_exception(Album, pk=album_id)
user = self.context.request.user
self_moderate = user.has_perm("sas.moderate_sasfile")
new = Picture(
parent=album,
name=picture.name,
file=picture,
owner=user,
is_moderated=self_moderate,
is_folder=False,
mime_type=picture.content_type,
)
if self_moderate:
new.moderator = user
try:
new.generate_thumbnails()
new.full_clean()
new.save()
except ValidationError as e:
return self.create_response({"detail": dict(e)}, status_code=409)
@route.get(
"/{picture_id}/identified",
permissions=[IsAuthenticated, CanView],

View File

@ -1,6 +1,7 @@
from typing import Any
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from core.models import User
@ -11,55 +12,28 @@ from sas.models import Album, Picture, PictureModerationRequest
from sas.widgets.ajax_select import AutoCompleteSelectAlbum
class SASForm(forms.Form):
album_name = forms.CharField(
label=_("Add a new album"), max_length=Album.NAME_MAX_LENGTH, required=False
)
images = MultipleImageField(
label=_("Upload images"),
required=False,
)
class AlbumCreateForm(forms.ModelForm):
class Meta:
model = Album
fields = ["name", "parent"]
labels = {"name": _("Add a new album")}
widgets = {"parent": forms.HiddenInput}
def process(self, parent, owner, files, *, automodere=False):
try:
if self.cleaned_data["album_name"] != "":
album = Album(
parent=parent,
name=self.cleaned_data["album_name"],
owner=owner,
is_moderated=automodere,
)
album.clean()
album.save()
except Exception as e:
self.add_error(
None,
_("Error creating album %(album)s: %(msg)s")
% {"album": self.cleaned_data["album_name"], "msg": repr(e)},
)
for f in files:
new_file = Picture(
parent=parent,
name=f.name,
file=f,
owner=owner,
mime_type=f.content_type,
size=f.size,
is_folder=False,
is_moderated=automodere,
)
if automodere:
new_file.moderator = owner
try:
new_file.clean()
new_file.generate_thumbnails()
new_file.save()
except Exception as e:
self.add_error(
None,
_("Error uploading file %(file_name)s: %(msg)s")
% {"file_name": f, "msg": repr(e)},
)
def __init__(self, *args, owner: User, **kwargs):
super().__init__(*args, **kwargs)
self.instance.owner = owner
if owner.has_perm("sas.moderate_sasfile"):
self.instance.is_moderated = True
self.instance.moderator = owner
def clean(self):
if not self.instance.owner.can_edit(self.instance.parent):
raise ValidationError(_("You do not have the permission to do that"))
return super().clean()
class PictureUploadForm(forms.Form):
images = MultipleImageField(label=_("Upload images"), required=False)
class PictureEditForm(forms.ModelForm):

View File

@ -134,7 +134,6 @@ class Picture(SasFile):
self.thumbnail.name = new_extension_name
self.compressed = compressed
self.compressed.name = new_extension_name
self.save()
def rotate(self, degree):
for attr in ["file", "compressed", "thumbnail"]:
@ -235,6 +234,8 @@ class Album(SasFile):
return Album.objects.filter(parent=self)
def get_absolute_url(self):
if self.id == settings.SITH_SAS_ROOT_DIR_ID:
return reverse("sas:main")
return reverse("sas:album", kwargs={"album_id": self.id})
def get_download_url(self):

View File

@ -5,8 +5,10 @@ import {
type AlbumSchema,
type PictureSchema,
type PicturesFetchPicturesData,
type PicturesUploadPictureErrors,
albumFetchAlbum,
picturesFetchPictures,
picturesUploadPicture,
} from "#openapi";
interface AlbumPicturesConfig {
@ -78,4 +80,49 @@ document.addEventListener("alpine:init", () => {
this.loading = false;
},
}));
Alpine.data("pictureUpload", (albumId: number) => ({
errors: [] as string[],
pictures: [],
sending: false,
progress: null as HTMLProgressElement,
init() {
this.progress = this.$refs.progress;
},
async sendPictures() {
const input = this.$refs.pictures as HTMLInputElement;
const files = input.files;
this.errors = [];
this.progress.value = 0;
this.progress.max = files.length;
this.sending = true;
for (const file of files) {
await this.sendPicture(file);
}
this.sending = false;
// This should trigger a reload of the pictures of the `picture` Alpine data
this.$dispatch("pictures-upload-done");
},
async sendPicture(file: File) {
const res = await picturesUploadPicture({
// biome-ignore lint/style/useNamingConvention: api is snake_case
body: { album_id: albumId, picture: file },
});
if (!res.response.ok) {
let msg = "";
if (res.response.status === 422) {
msg = (res.error as PicturesUploadPictureErrors[422]).detail
.map((err: Record<"ctx", Record<"error", string>>) => err.ctx.error)
.join(" ; ");
} else {
msg = Object.values(res.error.detail).join(" ; ");
}
this.errors.push(`${file.name} : ${msg}`);
}
this.progress.value += 1;
},
}));
});

View File

@ -73,7 +73,7 @@
<div class="text">{% trans %}To be moderated{% endtrans %}</div>
</template>
</div>
{% if edit_mode %}
{% if is_sas_admin %}
<input type="checkbox" name="file_list" :value="album.id">
{% endif %}
</a>
@ -86,7 +86,7 @@
<h4>{% trans %}Pictures{% endtrans %}</h4>
<br>
{{ download_button(_("Download album")) }}
<div class="photos" :aria-busy="loading">
<div class="photos" :aria-busy="loading" @pictures-upload-done.window="fetchPictures">
<template x-for="picture in getPage(page)">
<a :href="picture.sas_url">
<div class="photo" :class="{not_moderated: !picture.is_moderated}">
@ -110,13 +110,28 @@
{% if is_sas_admin %}
</form>
<form class="add-files" id="upload_form" action="" method="post" enctype="multipart/form-data">
{{ album_create_fragment }}
<form
class="add-files"
id="upload_form"
x-data="pictureUpload({{ album.id }})"
@submit.prevent="sendPictures()"
>
{% csrf_token %}
<div class="inputs">
{{ form.as_p() }}
<p>
<label for="{{ upload_form.images.id_for_label }}">{{ upload_form.images.label }} :</label>
{{ upload_form.images|add_attr("x-ref=pictures") }}
<span class="helptext">{{ upload_form.images.help_text }}</span>
</p>
<input type="submit" value="{% trans %}Upload{% endtrans %}" />
<progress x-ref="progress" x-show="sending"></progress>
</div>
<ul class="errorlist">
<template x-for="error in errors">
<li class="error" x-text="error"></li>
</template>
</ul>
</form>
{% endif %}
@ -126,115 +141,3 @@
{{ timezone.now() - start }}
</p>
{% endblock %}
{% block script %}
{{ super() }}
<script>
// Todo: migrate to alpine.js if we have some time
$("form#upload_form").submit(function (event) {
let formData = new FormData($(this)[0]);
if(!formData.get('album_name') && !formData.get('images').name)
return false;
if(!formData.get('images').name) {
return true;
}
event.preventDefault();
let errorList;
if((errorList = this.querySelector('#upload_form ul.errorlist.nonfield')) === null) {
errorList = document.createElement('ul');
errorList.classList.add('errorlist', 'nonfield');
this.insertBefore(errorList, this.firstElementChild);
}
while(errorList.childElementCount > 0)
errorList.removeChild(errorList.firstElementChild);
let progress;
if((progress = this.querySelector('progress')) === null) {
progress = document.createElement('progress');
progress.value = 0;
let p = document.createElement('p');
p.appendChild(progress);
this.insertBefore(p, this.lastElementChild);
}
let dataHolder;
if(formData.get('album_name')) {
dataHolder = new FormData();
dataHolder.set('csrfmiddlewaretoken', '{{ csrf_token }}');
dataHolder.set('album_name', formData.get('album_name'));
$.ajax({
method: 'POST',
url: "{{ url('sas:album_upload', album_id=object.id) }}",
data: dataHolder,
processData: false,
contentType: false,
success: onSuccess
});
}
let images = formData.getAll('images');
let imagesCount = images.length;
let completeCount = 0;
let poolSize = 1;
let imagePool = [];
while(images.length > 0 && imagePool.length < poolSize) {
let image = images.shift();
imagePool.push(image);
sendImage(image);
}
function sendImage(image) {
dataHolder = new FormData();
dataHolder.set('csrfmiddlewaretoken', '{{ csrf_token }}');
dataHolder.set('images', image);
$.ajax({
method: 'POST',
url: "{{ url('sas:album_upload', album_id=object.id) }}",
data: dataHolder,
processData: false,
contentType: false,
})
.fail(onSuccess.bind(undefined, image))
.done(onSuccess.bind(undefined, image))
.always(next.bind(undefined, image));
}
function next(image, _, __) {
let index = imagePool.indexOf(image);
let nextImage = images.shift();
if(index !== -1)
imagePool.splice(index, 1);
if(nextImage) {
imagePool.push(nextImage);
sendImage(nextImage);
}
}
function onSuccess(image, data, _, __) {
let errors = [];
if ($(data.responseText).find('.errorlist.nonfield')[0])
errors = Array.from($(data.responseText).find('.errorlist.nonfield')[0].children);
while(errors.length > 0)
errorList.appendChild(errors.shift());
progress.value = ++completeCount / imagesCount;
if(progress.value === 1 && errorList.children.length === 0)
document.location.reload()
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,18 @@
<form
class="add-files"
hx-post="{{ url("sas:album_create") }}"
hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML"
>
{% csrf_token %}
<div class="inputs">
<div>
<label for="{{ form.name.id_for_label }}">{{ form.name.label }}</label>
{{ form.name }}
</div>
{{ form.parent }}
<input type="submit" value="{% trans %}Create{% endtrans %}" />
</div>
{{ form.non_field_errors() }}
{{ form.name.errors }}
</form>

View File

@ -61,23 +61,8 @@
{% if is_sas_admin %}
</form>
<br>
<form class="add-files" action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="inputs">
<div>
<label for="{{ form.album_name.name }}">{{ form.album_name.label }}</label>
{{ form.album_name }}
</div>
<input type="submit" value="{% trans %}Create{% endtrans %}" />
</div>
{{ form.non_field_errors() }}
{{ form.album_name.errors }}
</form>
{{ album_create_fragment }}
{% endif %}
{% endif %}
</main>

View File

@ -1,13 +1,16 @@
import pytest
from django.conf import settings
from django.core.cache import cache
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db import transaction
from django.test import TestCase
from django.test import Client, TestCase
from django.urls import reverse
from model_bakery import baker
from model_bakery.recipe import Recipe
from core.baker_recipes import old_subscriber_user, subscriber_user
from core.models import Group, SithFile, User
from core.utils import RED_PIXEL_PNG
from sas.baker_recipes import picture_recipe
from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest
@ -241,3 +244,45 @@ class TestAlbumSearch(TestSas):
# - 1 for pagination
# - 1 for the actual results
self.client.get(reverse("api:search-album"))
@pytest.mark.django_db
def test_upload_picture(client: Client):
sas = SithFile.objects.get(pk=settings.SITH_SAS_ROOT_DIR_ID)
album = baker.make(Album, is_in_sas=True, parent=sas, name="test album")
user = baker.make(User, is_superuser=True)
client.force_login(user)
img = SimpleUploadedFile(
name="img.png", content=RED_PIXEL_PNG, content_type="image/png"
)
res = client.post(
reverse("api:upload_picture"), {"album_id": album.id, "picture": img}
)
assert res.status_code == 200
picture = Picture.objects.filter(parent_id=album.id).first()
assert picture is not None
assert picture.name == "img.png"
assert picture.owner == user
assert picture.file.name == "SAS/test album/img.png"
assert picture.compressed.name == ".compressed/SAS/test album/img.webp"
assert picture.thumbnail.name == ".thumbnails/SAS/test album/img.webp"
@pytest.mark.django_db
def test_upload_invalid_picture(client: Client):
sas = SithFile.objects.get(pk=settings.SITH_SAS_ROOT_DIR_ID)
album = baker.make(Album, is_in_sas=True, parent=sas, name="test album")
user = baker.make(User, is_superuser=True)
client.force_login(user)
file = SimpleUploadedFile(
name="file.txt",
content=b"azerty",
content_type="image/png", # the server shouldn't blindly trust the content_type
)
res = client.post(
reverse("api:upload_picture"), {"album_id": album.id, "picture": file}
)
assert res.status_code == 422
assert res.json()["detail"][0]["ctx"]["error"] == (
"Ce fichier n'est pas une image valide"
)

View File

@ -16,8 +16,8 @@
from django.urls import path
from sas.views import (
AlbumCreateFragment,
AlbumEditView,
AlbumUploadView,
AlbumView,
ModerationView,
PictureAskRemovalView,
@ -35,9 +35,6 @@ urlpatterns = [
path("", SASMainView.as_view(), name="main"),
path("moderation/", ModerationView.as_view(), name="moderation"),
path("album/<int:album_id>/", AlbumView.as_view(), name="album"),
path(
"album/<int:album_id>/upload/", AlbumUploadView.as_view(), name="album_upload"
),
path("album/<int:album_id>/edit/", AlbumEditView.as_view(), name="album_edit"),
path("album/<int:album_id>/preview/", send_album, name="album_preview"),
path("picture/<int:picture_id>/", PictureView.as_view(), name="picture"),
@ -59,4 +56,5 @@ urlpatterns = [
path(
"user/<int:user_id>/pictures/", UserPicturesView.as_view(), name="user_pictures"
),
path("fragment/album-create", AlbumCreateFragment.as_view(), name="album_create"),
]

View File

@ -16,48 +16,63 @@ from typing import Any
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, TemplateView
from django.views.generic.edit import FormMixin, FormView, UpdateView
from django.urls import reverse
from django.utils.safestring import SafeString
from django.views.generic import CreateView, DetailView, TemplateView
from django.views.generic.edit import FormView, UpdateView
from core.auth.mixins import CanEditMixin, CanViewMixin
from core.models import SithFile, User
from core.views import UseFragmentsMixin
from core.views.files import FileView, send_file
from core.views.mixins import FragmentMixin, FragmentRenderer
from core.views.user import UserTabsMixin
from sas.forms import (
AlbumCreateForm,
AlbumEditForm,
PictureEditForm,
PictureModerationRequestForm,
SASForm,
PictureUploadForm,
)
from sas.models import Album, Picture
class SASMainView(FormView):
form_class = SASForm
template_name = "sas/main.jinja"
success_url = reverse_lazy("sas:main")
class AlbumCreateFragment(FragmentMixin, CreateView):
model = Album
form_class = AlbumCreateForm
template_name = "sas/fragments/album_create_form.jinja"
reload_on_redirect = True
def post(self, request, *args, **kwargs):
self.form = self.get_form()
parent = SithFile.objects.filter(id=settings.SITH_SAS_ROOT_DIR_ID).first()
files = request.FILES.getlist("images")
root = User.objects.filter(username="root").first()
if request.user.is_authenticated and request.user.is_in_group(
pk=settings.SITH_GROUP_SAS_ADMIN_ID
):
if self.form.is_valid():
self.form.process(
parent=parent, owner=root, files=files, automodere=True
)
if self.form.is_valid():
return super().form_valid(self.form)
else:
self.form.add_error(None, _("You do not have the permission to do that"))
return self.form_invalid(self.form)
def get_form_kwargs(self):
return super().get_form_kwargs() | {"owner": self.request.user}
def render_fragment(
self, request, owner: User | None = None, **kwargs
) -> SafeString:
self.object = None
self.owner = owner or self.request.user
return super().render_fragment(request, **kwargs)
def get_success_url(self):
parent = self.object.parent
parent.__class__ = Album
return parent.get_absolute_url()
class SASMainView(UseFragmentsMixin, TemplateView):
template_name = "sas/main.jinja"
def get_fragments(self) -> dict[str, FragmentRenderer]:
form_init = {"parent": SithFile.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID)}
return {
"album_create_fragment": AlbumCreateFragment.as_fragment(initial=form_init)
}
def get_fragment_data(self) -> dict[str, dict[str, Any]]:
root_user = User.objects.get(pk=settings.SITH_ROOT_USER_ID)
return {"album_create_fragment": {"owner": root_user}}
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
@ -104,88 +119,45 @@ def send_thumb(request, picture_id):
return send_file(request, picture_id, Picture, "thumbnail")
class AlbumUploadView(CanViewMixin, DetailView, FormMixin):
class AlbumView(CanViewMixin, UseFragmentsMixin, DetailView):
model = Album
form_class = SASForm
pk_url_kwarg = "album_id"
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.file:
self.object.generate_thumbnail()
self.form = self.get_form()
parent = SithFile.objects.filter(id=self.object.id).first()
files = request.FILES.getlist("images")
if request.user.is_subscribed and self.form.is_valid():
self.form.process(
parent=parent,
owner=request.user,
files=files,
automodere=(
request.user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
or request.user.is_root
),
)
if self.form.is_valid():
return HttpResponse(str(self.form.errors), status=200)
return HttpResponse(str(self.form.errors), status=500)
class AlbumView(CanViewMixin, DetailView, FormMixin):
model = Album
form_class = SASForm
pk_url_kwarg = "album_id"
template_name = "sas/album.jinja"
def get_fragments(self) -> dict[str, FragmentRenderer]:
return {
"album_create_fragment": AlbumCreateFragment.as_fragment(
initial={"parent": self.object}
)
}
def dispatch(self, request, *args, **kwargs):
try:
self.asked_page = int(request.GET.get("page", 1))
except ValueError as e:
raise Http404 from e
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
self.form = self.get_form()
if "clipboard" not in request.session:
request.session["clipboard"] = []
return super().get(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.file:
self.object.generate_thumbnail()
self.form = self.get_form()
if "clipboard" not in request.session:
request.session["clipboard"] = []
if request.user.can_edit(self.object): # Handle the copy-paste functions
FileView.handle_clipboard(request, self.object)
parent = SithFile.objects.filter(id=self.object.id).first()
files = request.FILES.getlist("images")
if request.user.is_authenticated and request.user.is_subscribed:
if self.form.is_valid():
self.form.process(
parent=parent,
owner=request.user,
files=files,
automodere=request.user.is_in_group(
pk=settings.SITH_GROUP_SAS_ADMIN_ID
),
)
if self.form.is_valid():
return super().form_valid(self.form)
else:
self.form.add_error(None, _("You do not have the permission to do that"))
return self.form_invalid(self.form)
return HttpResponseRedirect(self.request.path)
def get_success_url(self):
return reverse("sas:album", kwargs={"album_id": self.object.id})
def get_fragment_data(self) -> dict[str, dict[str, Any]]:
return {"album_create_fragment": {"owner": self.request.user}}
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["form"] = self.form
kwargs["clipboard"] = SithFile.objects.filter(
id__in=self.request.session["clipboard"]
)
if ids := self.request.session.get("clipboard", None):
kwargs["clipboard"] = SithFile.objects.filter(id__in=ids)
kwargs["upload_form"] = PictureUploadForm()
# if True, the albums will be fetched with a request to the API
# if False, the section won't be displayed at all
kwargs["show_albums"] = (
Album.objects.viewable_by(self.request.user)
.filter(parent_id=self.object.id)