Merge pull request #775 from ae-utbm/user-pictures-ajax

Render user picture page with ajax to improve performances
This commit is contained in:
thomas girod 2024-08-18 12:40:07 +02:00 committed by GitHub
commit 4036bfd703
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 95 additions and 100 deletions

View File

@ -17,57 +17,53 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main> <main x-data="user_pictures">
{% 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="is_downloading"
class="btn btn-blue" class="btn btn-blue"
@click="download_zip()" @click="download_zip()"
> >
<i class="fa fa-download"></i> <i class="fa fa-download"></i>
{% trans %}Download all my pictures{% endtrans %} {% trans %}Download all my pictures{% endtrans %}
</button> </button>
<progress x-ref="progress" x-show="in_progress"></progress> <progress x-ref="progress" x-show="is_downloading"></progress>
</div> </div>
{% endif %} {% endif %}
{% for album, pictures in albums|items %}
<h4>{{ album }}</h4> <template x-for="[album, pictures] in Object.entries(albums)" x-cloak>
<section>
<br /> <br />
<h4 x-text="album"></h4>
<div class="photos"> <div class="photos">
{% for picture in pictures %} <template x-for="picture in pictures">
{% 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>
</a> </a>
{% else %} </template>
<div>
<div class="photo">
<div class="text">{% trans %}Picture Unavailable{% endtrans %}</div>
</div> </div>
</div> </section>
{% endif %} </template>
{% endfor %} <div class="photos" :aria-busy="loading"></div>
</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 +79,17 @@
*/ */
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("picture_download", () => ({ Alpine.data("user_pictures", () => ({
in_progress: false, is_downloading: 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 +103,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))
); );
@ -110,11 +120,10 @@
async download_zip(){ async download_zip(){
this.in_progress = true; this.is_downloading = 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 +133,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,
@ -134,10 +143,9 @@
})); }));
await zipWriter.close(); await zipWriter.close();
this.in_progress = false; this.is_downloading = false;
} }
})) }))
}); });
</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>