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,58 +17,54 @@
{% endblock %}
{% block content %}
<main>
{% if user.id == object.id and albums|length > 0 %}
<div x-data="picture_download" x-cloak>
<main x-data="user_pictures">
{% if user.id == object.id %}
<div x-show="pictures.length > 0" x-cloak>
<button
:disabled="in_progress"
:disabled="is_downloading"
class="btn btn-blue"
@click="download_zip()"
>
<i class="fa fa-download"></i>
{% trans %}Download all my pictures{% endtrans %}
</button>
<progress x-ref="progress" x-show="in_progress"></progress>
<progress x-ref="progress" x-show="is_downloading"></progress>
</div>
{% endif %}
{% for album, pictures in albums|items %}
<h4>{{ album }}</h4>
<br />
<div class="photos">
{% for picture in pictures %}
{% if picture.can_be_viewed_by(user) %}
<a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
<template x-for="[album, pictures] in Object.entries(albums)" x-cloak>
<section>
<br />
<h4 x-text="album"></h4>
<div class="photos">
<template x-for="picture in pictures">
<a :href="`/sas/picture/${picture.id}#pict`">
<div
class="photo{% if not picture.is_moderated %} not_moderated{% endif %}"
style="background-image: url('{% if picture.file %}{{ picture.get_download_thumb_url() }}{% else %}{{ static('core/img/sas.jpg') }}{% endif %}');"
class="photo"
: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="text">{% trans %}To be moderated{% endtrans %}</div>
{% else %}
</template>
<template x-if="picture.is_moderated">
<div class="text">&nbsp;</div>
{% endif %}
</template>
</div>
</a>
{% else %}
<div>
<div class="photo">
<div class="text">{% trans %}Picture Unavailable{% endtrans %}</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
<br>
{% endfor %}
</template>
</div>
</section>
</template>
<div class="photos" :aria-busy="loading"></div>
</main>
{% endblock content %}
{% block script %}
{{ super() }}
{% if user.id == object.id %}
<script>
<script>
/**
* @typedef Picture
* @property {number} id
@ -82,62 +78,74 @@
* @property {string} album
*/
document.addEventListener("alpine:init", () => {
Alpine.data("picture_download", () => ({
in_progress: false,
document.addEventListener("alpine:init", () => {
Alpine.data("user_pictures", () => ({
is_downloading: false,
loading: true,
pictures: [],
albums: {},
/**
* @return {Promise<Picture[]>}
*/
async get_pictures() {
{# The API forbids to get more than 199 items at once
from paginated routes.
In order to download all the user pictures, it may be needed
to performs multiple requests #}
const max_per_page = 199;
const url = "{{ url("api:pictures") }}"
+ "?users_identified={{ object.id }}"
+ `&page_size=${max_per_page}`;
let promises = [];
const nb_pages = Math.ceil({{ nb_pictures }} / max_per_page);
for (let i = 1; i <= nb_pages; i++) {
promises.push(
fetch(url + `&page=${i}`).then(res => res.json().then(json => json.results))
);
}
return (await Promise.all(promises)).flat()
},
async init() {
this.pictures = await this.get_pictures();
this.albums = Object.groupBy(this.pictures, ({album}) => album);
this.loading = false;
},
/**
* @return {Promise<Picture[]>}
*/
async get_pictures() {
{# The API forbids to get more than 199 items at once
from paginated routes.
In order to download all the user pictures, it may be needed
to performs multiple requests #}
const max_per_page = 199;
const url = "{{ url("api:pictures") }}"
+ "?users_identified={{ object.id }}"
+ `&page_size=${max_per_page}`;
async download_zip(){
this.in_progress = true;
const bar = this.$refs.progress;
bar.value = 0;
const pictures = await this.get_pictures();
bar.max = pictures.length;
let first_page = (await ( await fetch(url)).json());
let promises = [first_page.results];
const fileHandle = await window.showSaveFilePicker({
_preferPolyfill: false,
suggestedName: "{%- trans -%} pictures {%- endtrans -%}.zip",
types: {},
excludeAcceptAllOption: false,
})
const zipWriter = new zip.ZipWriter(await fileHandle.createWritable());
const nb_pictures = first_page.count
const nb_pages = Math.ceil(nb_pictures / max_per_page);
await Promise.all(pictures.map(p => {
const img_name = p.album + "/IMG_" + p.date.replaceAll(/[:\-]/g, "_") + p.name.slice(p.name.lastIndexOf("."));
return zipWriter.add(
img_name,
new zip.HttpReader(p.full_size_url),
{level: 9, lastModDate: new Date(p.date), onstart: () => bar.value += 1}
);
}));
await zipWriter.close();
this.in_progress = false;
for (let i = 2; i <= nb_pages; i++) {
promises.push(
fetch(url + `&page=${i}`).then(res => res.json().then(json => json.results))
);
}
}))
});
</script>
{% endif %}
return (await Promise.all(promises)).flat()
},
async download_zip(){
this.is_downloading = true;
const bar = this.$refs.progress;
bar.value = 0;
bar.max = this.pictures.length;
const fileHandle = await window.showSaveFilePicker({
_preferPolyfill: false,
suggestedName: "{%- trans -%} pictures {%- endtrans -%}.zip",
types: {},
excludeAcceptAllOption: false,
})
const zipWriter = new zip.ZipWriter(await fileHandle.createWritable());
await Promise.all(this.pictures.map(p => {
const img_name = p.album + "/IMG_" + p.date.replaceAll(/[:\-]/g, "_") + p.name.slice(p.name.lastIndexOf("."));
return zipWriter.add(
img_name,
new zip.HttpReader(p.full_size_url),
{level: 9, lastModDate: new Date(p.date), onstart: () => bar.value += 1}
);
}));
await zipWriter.close();
this.is_downloading = false;
}
}))
});
</script>
{% endblock script %}

View File

@ -21,7 +21,6 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import itertools
import logging
# 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.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import F
from django.forms import CheckboxSelectMultiple
from django.forms.models import modelform_factory
from django.http import Http404, HttpResponse
@ -70,7 +68,6 @@ from core.views.forms import (
UserProfileForm,
)
from counter.forms import StudentCardForm
from sas.models import Picture
from subscription.models import Subscription
from trombi.views import UserTrombiForm
@ -312,20 +309,6 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
template_name = "core/user_pictures.jinja"
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):
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">
<template x-for="picture in pictures.results">
<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">
<div class="overlay">&nbsp;</div>
<div class="text">{% trans %}To be moderated{% endtrans %}</div>