Use Alpine and the API for SAS picture upload

This commit is contained in:
imperosol 2025-03-29 18:19:58 +01:00
parent 41d33f3e9e
commit 305f37a806
6 changed files with 52 additions and 152 deletions

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import {
type PicturesFetchPicturesData, type PicturesFetchPicturesData,
albumFetchAlbum, albumFetchAlbum,
picturesFetchPictures, picturesFetchPictures,
picturesUploadPicture,
} from "#openapi"; } from "#openapi";
interface AlbumPicturesConfig { interface AlbumPicturesConfig {
@ -78,4 +79,40 @@ document.addEventListener("alpine:init", () => {
this.loading = false; 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.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) {
this.errors.push(`${file.name} : ${res.error.detail}`);
}
this.progress.value += 1;
},
}));
}); });

View File

@ -73,7 +73,7 @@
<div class="text">{% trans %}To be moderated{% endtrans %}</div> <div class="text">{% trans %}To be moderated{% endtrans %}</div>
</template> </template>
</div> </div>
{% if edit_mode %} {% if is_sas_admin %}
<input type="checkbox" name="file_list" :value="album.id"> <input type="checkbox" name="file_list" :value="album.id">
{% endif %} {% endif %}
</a> </a>
@ -86,7 +86,7 @@
<h4>{% trans %}Pictures{% endtrans %}</h4> <h4>{% trans %}Pictures{% endtrans %}</h4>
<br> <br>
{{ download_button(_("Download album")) }} {{ 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)"> <template x-for="picture in getPage(page)">
<a :href="picture.sas_url"> <a :href="picture.sas_url">
<div class="photo" :class="{not_moderated: !picture.is_moderated}"> <div class="photo" :class="{not_moderated: !picture.is_moderated}">
@ -141,115 +141,3 @@
{{ timezone.now() - start }} {{ timezone.now() - start }}
</p> </p>
{% endblock %} {% 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

@ -18,7 +18,6 @@ from django.urls import path
from sas.views import ( from sas.views import (
AlbumCreateFragment, AlbumCreateFragment,
AlbumEditView, AlbumEditView,
AlbumUploadView,
AlbumView, AlbumView,
ModerationView, ModerationView,
PictureAskRemovalView, PictureAskRemovalView,
@ -36,9 +35,6 @@ urlpatterns = [
path("", SASMainView.as_view(), name="main"), path("", SASMainView.as_view(), name="main"),
path("moderation/", ModerationView.as_view(), name="moderation"), path("moderation/", ModerationView.as_view(), name="moderation"),
path("album/<int:album_id>/", AlbumView.as_view(), name="album"), 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>/edit/", AlbumEditView.as_view(), name="album_edit"),
path("album/<int:album_id>/preview/", send_album, name="album_preview"), path("album/<int:album_id>/preview/", send_album, name="album_preview"),
path("picture/<int:picture_id>/", PictureView.as_view(), name="picture"), path("picture/<int:picture_id>/", PictureView.as_view(), name="picture"),

View File

@ -16,22 +16,25 @@ from typing import Any
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied 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.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.safestring import SafeString
from django.views.generic import DetailView, TemplateView from django.views.generic import CreateView, DetailView, TemplateView
from django.views.generic.edit import FormMixin, FormView, UpdateView from django.views.generic.edit import FormView, UpdateView
from core.auth.mixins import CanEditMixin, CanViewMixin from core.auth.mixins import CanEditMixin, CanViewMixin
from core.models import SithFile, User from core.models import SithFile, User
from core.views import UseFragmentsMixin
from core.views.files import FileView, send_file from core.views.files import FileView, send_file
from core.views.mixins import FragmentMixin, FragmentRenderer
from core.views.user import UserTabsMixin from core.views.user import UserTabsMixin
from sas.forms import ( from sas.forms import (
AlbumCreateForm,
AlbumEditForm, AlbumEditForm,
PictureEditForm, PictureEditForm,
PictureModerationRequestForm, PictureModerationRequestForm,
SASForm, PictureUploadForm,
) )
from sas.models import Album, Picture from sas.models import Album, Picture
@ -116,32 +119,6 @@ def send_thumb(request, picture_id):
return send_file(request, picture_id, Picture, "thumbnail") return send_file(request, picture_id, Picture, "thumbnail")
class AlbumUploadView(CanViewMixin, DetailView, FormMixin):
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, UseFragmentsMixin, DetailView): class AlbumView(CanViewMixin, UseFragmentsMixin, DetailView):
model = Album model = Album
pk_url_kwarg = "album_id" pk_url_kwarg = "album_id"