Render user picture page with ajax to improve performances

This commit is contained in:
Antoine Bartuccio 2024-08-09 17:33:07 +02:00
parent 57a8215c6b
commit 0eeaf1ce21
3 changed files with 93 additions and 97 deletions

View File

@ -17,9 +17,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main> <main x-data="user_pictures" :aria-busy="loading">
{% if user.id == object.id and albums|length > 0 %} {% if user.id == object.id %}
<div x-data="picture_download" x-cloak> <div x-show="pictures.length > 0" x-cloak>
<button <button
:disabled="in_progress" :disabled="in_progress"
class="btn btn-blue" class="btn btn-blue"
@ -31,43 +31,40 @@
<progress x-ref="progress" x-show="in_progress"></progress> <progress x-ref="progress" x-show="in_progress"></progress>
</div> </div>
{% endif %} {% endif %}
{% for album, pictures in albums|items %}
<h4>{{ album }}</h4> <template x-for="title in [...Object.keys(albums)]">
<section>
<h4 x-text="title"></h4>
<br /> <br />
<div class="photos"> <div class="photos">
{% for picture in pictures %} <template x-for="picture in albums[title]">
{% if picture.can_be_viewed_by(user) %} <a :href="`/sas/picture/${picture.id}#pict`">
<a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
<div <div
class="photo{% if not picture.is_moderated %} not_moderated{% endif %}" class="photo"
style="background-image: url('{% if picture.file %}{{ picture.get_download_thumb_url() }}{% else %}{{ static('core/img/sas.jpg') }}{% endif %}');" :class="{not_moderated: !picture.is_moderated}"
:style="`background-image: url(${picture.thumb_url})`"
> >
{% if not picture.is_moderated %} <template x-if="!picture.is_moderated">
<div class="overlay">&nbsp;</div> <div class="overlay">&nbsp;</div>
<div class="text">{% trans %}To be moderated{% endtrans %}</div> <div class="text">{% trans %}To be moderated{% endtrans %}</div>
{% else %} </template>
<template x-if="picture.is_moderated">
<div class="text">&nbsp;</div> <div class="text">&nbsp;</div>
{% endif %} </template>
</div>
</div> </div>
</a> </a>
{% else %} </template>
<div>
<div class="photo">
<div class="text">{% trans %}Picture Unavailable{% endtrans %}</div>
</div> </div>
</div> </section>
{% endif %} </template>
{% endfor %}
</div>
<br>
{% endfor %}
</main> </main>
{% endblock content %} {% endblock content %}
{% block script %} {% block script %}
{{ super() }} {{ super() }}
{% if user.id == object.id %}
<script> <script>
/** /**
* @typedef Picture * @typedef Picture
@ -83,8 +80,17 @@
*/ */
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("picture_download", () => ({ Alpine.data("user_pictures", () => ({
in_progress: false, in_progress: false,
loading: true,
pictures: [],
albums: {},
async init() {
this.pictures = await this.get_pictures();
this.albums = Object.groupBy(this.pictures, ({album}) => album);
this.loading = false;
},
/** /**
* @return {Promise<Picture[]>} * @return {Promise<Picture[]>}
@ -98,9 +104,14 @@
const url = "{{ url("api:pictures") }}" const url = "{{ url("api:pictures") }}"
+ "?users_identified={{ object.id }}" + "?users_identified={{ object.id }}"
+ `&page_size=${max_per_page}`; + `&page_size=${max_per_page}`;
let promises = [];
const nb_pages = Math.ceil({{ nb_pictures }} / max_per_page); let first_page = (await ( await fetch(url)).json());
for (let i = 1; i <= nb_pages; i++) { let promises = [first_page.results];
const nb_pictures = first_page.count
const nb_pages = Math.ceil(nb_pictures / max_per_page);
for (let i = 2; i <= nb_pages; i++) {
promises.push( promises.push(
fetch(url + `&page=${i}`).then(res => res.json().then(json => json.results)) fetch(url + `&page=${i}`).then(res => res.json().then(json => json.results))
); );
@ -113,8 +124,7 @@
this.in_progress = true; this.in_progress = true;
const bar = this.$refs.progress; const bar = this.$refs.progress;
bar.value = 0; bar.value = 0;
const pictures = await this.get_pictures(); bar.max = this.pictures.length;
bar.max = pictures.length;
const fileHandle = await window.showSaveFilePicker({ const fileHandle = await window.showSaveFilePicker({
_preferPolyfill: false, _preferPolyfill: false,
@ -124,7 +134,7 @@
}) })
const zipWriter = new zip.ZipWriter(await fileHandle.createWritable()); const zipWriter = new zip.ZipWriter(await fileHandle.createWritable());
await Promise.all(pictures.map(p => { await Promise.all(this.pictures.map(p => {
const img_name = p.album + "/IMG_" + p.date.replaceAll(/[:\-]/g, "_") + p.name.slice(p.name.lastIndexOf(".")); const img_name = p.album + "/IMG_" + p.date.replaceAll(/[:\-]/g, "_") + p.name.slice(p.name.lastIndexOf("."));
return zipWriter.add( return zipWriter.add(
img_name, img_name,
@ -139,5 +149,4 @@
})) }))
}); });
</script> </script>
{% endif %}
{% endblock script %} {% endblock script %}

View File

@ -21,7 +21,6 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
import itertools
import logging import logging
# This file contains all the views that concern the user model # This file contains all the views that concern the user model
@ -33,7 +32,6 @@ from django.contrib.auth import login, views
from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import F
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
@ -70,7 +68,6 @@ from core.views.forms import (
UserProfileForm, UserProfileForm,
) )
from counter.forms import StudentCardForm from counter.forms import StudentCardForm
from sas.models import Picture
from subscription.models import Subscription from subscription.models import Subscription
from trombi.views import UserTrombiForm from trombi.views import UserTrombiForm
@ -312,20 +309,6 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
template_name = "core/user_pictures.jinja" template_name = "core/user_pictures.jinja"
current_tab = "pictures" current_tab = "pictures"
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
pictures = list(
Picture.objects.filter(people__user_id=self.object.id)
.order_by("-parent__date", "-date")
.annotate(album=F("parent__name"))
)
kwargs["nb_pictures"] = len(pictures)
kwargs["albums"] = {
album: list(picts)
for album, picts in itertools.groupby(pictures, lambda i: i.album)
}
return kwargs
def delete_user_godfather(request, user_id, godfather_id, is_father): def delete_user_godfather(request, user_id, godfather_id, is_father):
user_is_admin = request.user.is_root or request.user.is_board_member user_is_admin = request.user.is_root or request.user.is_board_member

View File

@ -64,7 +64,11 @@
<div class="photos" :aria-busy="loading"> <div class="photos" :aria-busy="loading">
<template x-for="picture in pictures.results"> <template x-for="picture in pictures.results">
<a :href="`/sas/picture/${picture.id}#pict`"> <a :href="`/sas/picture/${picture.id}#pict`">
<div class="photo" :style="`background-image: url(${picture.thumb_url})`"> <div
class="photo"
:class="{not_moderated: !picture.is_moderated}"
:style="`background-image: url(${picture.thumb_url})`"
>
<template x-if="!picture.is_moderated"> <template x-if="!picture.is_moderated">
<div class="overlay">&nbsp;</div> <div class="overlay">&nbsp;</div>
<div class="text">{% trans %}To be moderated{% endtrans %}</div> <div class="text">{% trans %}To be moderated{% endtrans %}</div>