From 20e8854467789a6a2332fa41f20f8cc253c2edc8 Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 9 Aug 2024 14:05:09 +0200 Subject: [PATCH 01/33] Fix operation logs --- core/admin.py | 9 ++++++++- counter/{app.py => apps.py} | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) rename counter/{app.py => apps.py} (92%) diff --git a/core/admin.py b/core/admin.py index c17f494b..e8436db0 100644 --- a/core/admin.py +++ b/core/admin.py @@ -16,7 +16,7 @@ from django.contrib import admin from django.contrib.auth.models import Group as AuthGroup -from core.models import Group, Page, SithFile, User +from core.models import Group, OperationLog, Page, SithFile, User admin.site.unregister(AuthGroup) @@ -53,3 +53,10 @@ class SithFileAdmin(admin.ModelAdmin): list_display = ("name", "owner", "size", "date", "is_in_sas") autocomplete_fields = ("parent", "owner", "moderator") search_fields = ("name", "parent__name") + + +@admin.register(OperationLog) +class OperationLogAdmin(admin.ModelAdmin): + list_display = ("label", "operator", "operation_type", "date") + search_fields = ("label", "date", "operation_type") + autocomplete_fields = ("operator",) diff --git a/counter/app.py b/counter/apps.py similarity index 92% rename from counter/app.py rename to counter/apps.py index 47c81a01..54e7ad4c 100644 --- a/counter/app.py +++ b/counter/apps.py @@ -16,7 +16,7 @@ # details. # # You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# this program; if not, write to the Free Software Foundation, Inc., 59 Temple # Place - Suite 330, Boston, MA 02111-1307, USA. # # From 0eeaf1ce214b595249a8bd1d6d9460459a1dfb55 Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 9 Aug 2024 17:33:07 +0200 Subject: [PATCH 02/33] Render user picture page with ajax to improve performances --- core/templates/core/user_pictures.jinja | 167 +++++++++++++----------- core/views/user.py | 17 --- sas/templates/sas/album.jinja | 6 +- 3 files changed, 93 insertions(+), 97 deletions(-) diff --git a/core/templates/core/user_pictures.jinja b/core/templates/core/user_pictures.jinja index e39e7c72..554604e0 100644 --- a/core/templates/core/user_pictures.jinja +++ b/core/templates/core/user_pictures.jinja @@ -17,9 +17,9 @@ {% endblock %} {% block content %} -
- {% if user.id == object.id and albums|length > 0 %} -
+
+ {% if user.id == object.id %} +
{% endif %} - {% for album, pictures in albums|items %} -

{{ album }}

-
-
- {% for picture in pictures %} - {% if picture.can_be_viewed_by(user) %} - + +
{% endblock content %} {% block script %} {{ super() }} - {% if user.id == object.id %} - - {% endif %} + return (await Promise.all(promises)).flat() + }, + + + async download_zip(){ + this.in_progress = 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.in_progress = false; + } + })) + }); + {% endblock script %} diff --git a/core/views/user.py b/core/views/user.py index 87a4b6a3..27602d22 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -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 diff --git a/sas/templates/sas/album.jinja b/sas/templates/sas/album.jinja index fed66e66..b7bfc4c1 100644 --- a/sas/templates/sas/album.jinja +++ b/sas/templates/sas/album.jinja @@ -64,7 +64,11 @@
+
-
- - -
+ + +
{% endblock content %} From b35e1a476edace0a164436bcabced478a2d569c3 Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 10 Aug 2024 18:38:04 +0200 Subject: [PATCH 08/33] Fix back function in album pagination --- core/static/core/js/script.js | 7 +++++-- sas/templates/sas/album.jinja | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/core/static/core/js/script.js b/core/static/core/js/script.js index f2c365ff..390deeb2 100644 --- a/core/static/core/js/script.js +++ b/core/static/core/js/script.js @@ -69,7 +69,7 @@ function getCSRFToken() { const initialUrlParams = new URLSearchParams(window.location.search); -function update_query_string(key, value) { +function update_query_string(key, value, push = true) { const url = new URL(window.location.href); if (!value) { // If the value is null, undefined or empty => delete it @@ -81,5 +81,8 @@ function update_query_string(key, value) { } else { url.searchParams.set(key, value); } - history.pushState(null, document.title, url.toString()); + + if (push){ + history.pushState(null, document.title, url.toString()); + } } diff --git a/sas/templates/sas/album.jinja b/sas/templates/sas/album.jinja index fed66e66..e9363f6f 100644 --- a/sas/templates/sas/album.jinja +++ b/sas/templates/sas/album.jinja @@ -129,13 +129,25 @@ Alpine.data("pictures", () => ({ pictures: {}, page: parseInt(initialUrlParams.get("page")) || 1, + pushstate: true, /* Used to avoid pushing a state on a back action */ loading: false, async init() { await this.fetch_pictures(); this.$watch("page", () => { - update_query_string("page", this.page === 1 ? null : this.page); - this.fetch_pictures() + update_query_string("page", + this.page === 1 ? null : this.page, + this.pushstate + ); + this.pushstate = true; + this.fetch_pictures(); + }); + + window.addEventListener("popstate", () => { + this.pushstate = false; + this.page = parseInt( + new URLSearchParams(window.location.search).get("page") + ) || 1; }); }, @@ -146,7 +158,7 @@ +`&page=${this.page}` +"&page_size={{ settings.SITH_SAS_IMAGES_PER_PAGE }}"; this.pictures = await (await fetch(url)).json(); - this.loading=false; + this.loading = false; }, nb_pages() { From 589119c9ee9e3a21c7babd2c6a9cafe977e63c6f Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 10 Aug 2024 23:32:50 +0200 Subject: [PATCH 09/33] Improve update_query_string with enum action --- core/static/core/js/script.js | 21 ++++++++++++++++++--- sas/templates/sas/album.jinja | 6 +++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/core/static/core/js/script.js b/core/static/core/js/script.js index 390deeb2..13613740 100644 --- a/core/static/core/js/script.js +++ b/core/static/core/js/script.js @@ -69,20 +69,35 @@ function getCSRFToken() { const initialUrlParams = new URLSearchParams(window.location.search); -function update_query_string(key, value, push = true) { +/** + * @readonly + * @enum {number} + */ +const History = { + PUSH: 1, + REPLACE: 2, +}; + +/** + * @param {string} key + * @param {string | string[] | null} value + * @param {History} action + */ +function update_query_string(key, value, action = History.REPLACE) { const url = new URL(window.location.href); if (!value) { // If the value is null, undefined or empty => delete it url.searchParams.delete(key) } else if (Array.isArray(value)) { - url.searchParams.delete(key) value.forEach((v) => url.searchParams.append(key, v)) } else { url.searchParams.set(key, value); } - if (push){ + if (action === History.PUSH) { history.pushState(null, document.title, url.toString()); + } else if (action === History.REPLACE) { + history.replaceState(null, document.title, url.toString()); } } diff --git a/sas/templates/sas/album.jinja b/sas/templates/sas/album.jinja index e9363f6f..05a1eab0 100644 --- a/sas/templates/sas/album.jinja +++ b/sas/templates/sas/album.jinja @@ -129,7 +129,7 @@ Alpine.data("pictures", () => ({ pictures: {}, page: parseInt(initialUrlParams.get("page")) || 1, - pushstate: true, /* Used to avoid pushing a state on a back action */ + pushstate: History.PUSH, /* Used to avoid pushing a state on a back action */ loading: false, async init() { @@ -139,12 +139,12 @@ this.page === 1 ? null : this.page, this.pushstate ); - this.pushstate = true; + this.pushstate = History.PUSH; this.fetch_pictures(); }); window.addEventListener("popstate", () => { - this.pushstate = false; + this.pushstate = History.REPLACE; this.page = parseInt( new URLSearchParams(window.location.search).get("page") ) || 1; From 2ec1f8cdc00323ebaccfac529f178d76754ab342 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 11 Aug 2024 14:58:05 +0200 Subject: [PATCH 10/33] Fix back action in uv guide --- pedagogy/templates/pedagogy/guide.jinja | 77 +++++++++++++++++++------ 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/pedagogy/templates/pedagogy/guide.jinja b/pedagogy/templates/pedagogy/guide.jinja index 1aa713e4..639ee0ca 100644 --- a/pedagogy/templates/pedagogy/guide.jinja +++ b/pedagogy/templates/pedagogy/guide.jinja @@ -114,13 +114,25 @@ @@ -141,34 +153,63 @@ Alpine.data("uv_search", () => ({ uvs: [], loading: false, - page: parseInt(initialUrlParams.get("page")) || page_default, - page_size: parseInt(initialUrlParams.get("page_size")) || page_size_default, - search: initialUrlParams.get("search") || "", - department: initialUrlParams.getAll("department"), - credit_type: initialUrlParams.getAll("credit_type"), - {# The semester is easier to use on the backend as an enum (spring/autumn/both/none) - and easier to use on the frontend as an array ([spring, autumn]). - Thus there is some conversion involved when both communicate together #} - semester: initialUrlParams.has("semester") ? - initialUrlParams.get("semester").split("_AND_") : [], + page: page_default, + page_size: page_size_default, + search: "", + department: [], + credit_type: [], + semester: [], + pushstate: History.PUSH, + + async initialize_args() { + let url = new URLSearchParams(window.location.search); + this.pushstate = History.REPLACE; + + this.page = parseInt(url.get("page")) || page_default;; + this.page_size = parseInt(url.get("page_size")) || page_size_default; + this.search = url.get("search") || ""; + this.department = url.getAll("department"); + this.credit_type = url.getAll("credit_type"); + {# The semester is easier to use on the backend as an enum (spring/autumn/both/none) + and easier to use on the frontend as an array ([spring, autumn]). + Thus there is some conversion involved when both communicate together #} + this.semester = url.has("semester") ? + url.get("semester").split("_AND_") : []; + + {# Wait for all watch callbacks to be called #} + await (new Promise(resolve => setTimeout(resolve, 100))); + + this.pushstate = History.PUSH; + }, async init() { + await this.initialize_args(); let search_params = ["search", "department", "credit_type", "semester"]; - let pagination_params = ["semester", "page"]; + let pagination_params = ["page", "page_size"]; + + this.fetch_data(); {# load initial data #} + search_params.forEach((param) => { - this.$watch(param, async () => { - {# Reset pagination on search #} + this.$watch(param, async (value) => { + if (this.pushstate != History.PUSH){ + {# This means that we are doing a mass param edit #} + return; + } + {# Reset pagination on search #} this.page = page_default; this.page_size = page_size_default; }); }); search_params.concat(pagination_params).forEach((param) => { this.$watch(param, async (value) => { - update_query_string(param, value); + console.log(param + " " + value) + update_query_string(param, value, this.pushstate); await this.fetch_data(); {# reload data on form change #} }); }); - await this.fetch_data(); {# load initial data #} + window.addEventListener("popstate", () => { + this.initialize_args(); + }); }, async fetch_data() { From 2a6c1f050d6f7281a4250e8e127e86408a2a3369 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 11 Aug 2024 15:11:51 +0200 Subject: [PATCH 11/33] Create a paginate_alpine macro --- core/templates/core/macros.jinja | 40 +++++++++++++++++++++++++ pedagogy/templates/pedagogy/guide.jinja | 25 ++-------------- sas/templates/sas/album.jinja | 24 ++------------- 3 files changed, 44 insertions(+), 45 deletions(-) diff --git a/core/templates/core/macros.jinja b/core/templates/core/macros.jinja index 2344de37..11014a64 100644 --- a/core/templates/core/macros.jinja +++ b/core/templates/core/macros.jinja @@ -116,6 +116,46 @@ {% endif %} {% endmacro %} +{% macro paginate_alpine(page, nb_pages) %} + {# Add pagination buttons for ajax based content with alpine + + Notes: + This can only be used in the scope of your alpine datastore + + Notes: + You might need to listen to the "popstate" event in your code + to update the current page you are on when the user goes back in + it's browser history with the back arrow + + Parameters: + page (str): name of the alpine page variable in your datastore + nb_page (str): call to a javascript function or variable returning + the maximum number of pages to paginate + #} + +{% endmacro %} + {% macro paginate(page_obj, paginator, js_action) %} {% set js = js_action|default('') %} {% if page_obj.has_previous() or page_obj.has_next() %} diff --git a/pedagogy/templates/pedagogy/guide.jinja b/pedagogy/templates/pedagogy/guide.jinja index 639ee0ca..e2ab4d22 100644 --- a/pedagogy/templates/pedagogy/guide.jinja +++ b/pedagogy/templates/pedagogy/guide.jinja @@ -1,4 +1,5 @@ {% extends "core/base.jinja" %} +{% from 'core/macros.jinja' import paginate_alpine %} {% block title %} {% trans %}UV Guide{% endtrans %} @@ -113,29 +114,7 @@ - + {{ paginate_alpine("page", "max_page()") }} + {% endblock %} diff --git a/core/templates/core/user_pictures.jinja b/core/templates/core/user_pictures.jinja index 18527a3c..bd5f1d44 100644 --- a/core/templates/core/user_pictures.jinja +++ b/core/templates/core/user_pictures.jinja @@ -10,7 +10,6 @@ window.showSaveFilePicker = showSaveFilePicker; /* Export function to normal javascript */ - {% endblock %} {% block title %} @@ -33,31 +32,29 @@ {% endif %} -