Merge pull request #858 from ae-utbm/jsstandard

Add biome to format js files
This commit is contained in:
thomas girod 2024-10-08 23:45:20 +02:00 committed by GitHub
commit b969513d94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 10820 additions and 6681 deletions

View File

@ -8,6 +8,11 @@ repos:
args: ["--fix", "--silent"]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/biomejs/pre-commit
rev: "v0.1.0" # Use the sha / tag you want to point at
hooks:
- id: biome-check
additional_dependencies: ["@biomejs/biome@1.9.3"]
- repo: https://github.com/rtts/djhtml
rev: 3.0.6
hooks:

View File

@ -5,6 +5,7 @@
[![CI status](https://github.com/ae-utbm/sith/actions/workflows/ci.yml/badge.svg)](#)
[![Docs status](https://github.com/ae-utbm/sith/actions/workflows/deploy_docs.yml/badge.svg)](https://ae-utbm.github.io/sith)
[![Built with Material for MkDocs](https://img.shields.io/badge/Material_for_MkDocs-526CFE?style=default&logo=MaterialForMkDocs&logoColor=white)](https://squidfunk.github.io/mkdocs-material/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![discord](https://img.shields.io/discord/971448179075731476?label=discord&logo=discord&style=default)](https://discord.gg/xk9wfpsufm)
### This is the source code of the UTBM's student association available at [https://ae.utbm.fr/](https://ae.utbm.fr/).

29
biome.json Normal file
View File

@ -0,0 +1,29 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"ignore": ["core/static/vendored", "*.min.*", "staticfiles/generated"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"lineWidth": 88
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"all": true
}
},
"javascript": {
"globals": ["Alpine", "$", "jQuery", "gettext", "interpolate"]
}
}

View File

@ -1,24 +1,23 @@
$(document).ready(function(){
$("#poster_list #view").click(function(e){
$(document).ready(() => {
$("#poster_list #view").click(() => {
$("#view").removeClass("active");
});
$("#poster_list .poster .image").click(function(e){
el = $(e.target);
if(el.hasClass("image"))
el = el.find("img")
$("#poster_list .poster .image").click((e) => {
let el = $(e.target);
if (el.hasClass("image")) {
el = el.find("img");
}
$("#poster_list #view #placeholder").html(el.clone());
$("#view").addClass("active");
});
$(document).keyup(function(e) {
if (e.keyCode == 27) { // escape key maps to keycode `27`
$(document).keyup((e) => {
if (e.keyCode === 27) {
// escape key maps to keycode `27`
e.preventDefault();
$("#view").removeClass("active");
}
});
});

View File

@ -1,14 +1,11 @@
$(document).ready(function(){
$(document).ready(() => {
const transitionTime = 1000;
transition_time = 1000;
i = 0;
max = $("#slideshow .slide").length;
next_trigger = 0
let i = 0;
const max = $("#slideshow .slide").length;
function enterFullscreen() {
element = document.getElementById("slideshow");
const element = document.getElementById("slideshow");
$(element).addClass("fullscreen");
if (element.requestFullscreen) {
element.requestFullscreen();
@ -22,7 +19,7 @@ $(document).ready(function(){
}
function exitFullscreen() {
element = document.getElementById("slideshow");
const element = document.getElementById("slideshow");
$(element).removeClass("fullscreen");
if (document.exitFullscreen) {
document.exitFullscreen();
@ -35,84 +32,67 @@ $(document).ready(function(){
}
}
function init_progress_bar()
{
function initProgressBar() {
$("#slideshow #progress_bar").css("transition", "none");
$("#slideshow #progress_bar").removeClass("progress");
$("#slideshow #progress_bar").addClass("init");
}
function start_progress_bar(display_time)
{
function startProgressBar(displayTime) {
$("#slideshow #progress_bar").removeClass("init");
$("#slideshow #progress_bar").addClass("progress");
$("#slideshow #progress_bar").css("transition", "width " + display_time + "s linear")
$("#slideshow #progress_bar").css("transition", `width ${displayTime}s linear`);
}
function next()
{
init_progress_bar();
slide = $($("#slideshow .slide").get(i % max));
function next() {
initProgressBar();
const slide = $($("#slideshow .slide").get(i % max));
slide.removeClass("center");
slide.addClass("left");
next_slide = $($("#slideshow .slide").get((i + 1) % max));
next_slide.removeClass("right");
next_slide.addClass("center");
display_time = next_slide.attr("display_time") || 2;
const nextSlide = $($("#slideshow .slide").get((i + 1) % max));
nextSlide.removeClass("right");
nextSlide.addClass("center");
const displayTime = nextSlide.attr("display_time") || 2;
$("#slideshow .bullet").removeClass("active");
bullet = $("#slideshow .bullet")[(i + 1) % max];
const bullet = $("#slideshow .bullet")[(i + 1) % max];
$(bullet).addClass("active");
i = (i + 1) % max;
setTimeout(function(){
others_left = $("#slideshow .slide.left");
others_left.removeClass("left");
others_left.addClass("right");
start_progress_bar(display_time);
next_trigger = setTimeout(next, display_time * 1000);
}, transition_time);
setTimeout(() => {
const othersLeft = $("#slideshow .slide.left");
othersLeft.removeClass("left");
othersLeft.addClass("right");
startProgressBar(displayTime);
setTimeout(next, displayTime * 1000);
}, transitionTime);
}
display_time = $("#slideshow .center").attr("display_time");
init_progress_bar();
setTimeout(function(){
const displayTime = $("#slideshow .center").attr("display_time");
initProgressBar();
setTimeout(() => {
if (max > 1) {
start_progress_bar(display_time);
setTimeout(next, display_time * 1000);
startProgressBar(displayTime);
setTimeout(next, displayTime * 1000);
}
}, 10);
$("#slideshow").click(function(e){
if(!$("#slideshow").hasClass("fullscreen"))
{
console.log("Entering fullscreen ...");
enterFullscreen();
$("#slideshow").click(() => {
if ($("#slideshow").hasClass("fullscreen")) {
exitFullscreen();
} else {
console.log("Exiting fullscreen ...");
exitFullscreen();
enterFullscreen();
}
});
$(document).keyup(function(e) {
if (e.keyCode == 27) { // escape key maps to keycode `27`
$(document).keyup((e) => {
if (e.keyCode === 27) {
// escape key maps to keycode `27`
e.preventDefault();
console.log("Exiting fullscreen ...");
exitFullscreen();
}
});
});

View File

@ -27,7 +27,7 @@
<div id="posters">
{% if poster_list.count() == 0 %}
{% if poster_list.count() == 0 %}
<div id="no-posters">{% trans %}No posters{% endtrans %}</div>
{% else %}

View File

@ -1 +1,14 @@
[{"pk": 1, "fields": {"permissions": [1, 2, 3, 7, 8, 9, 4, 5, 6, 10, 11, 12, 19, 20, 21, 22, 25, 23, 24, 16, 17, 18, 13, 14, 15]}, "model": "core.group"}, {"pk": 2, "fields": {"permissions": [25]}, "model": "core.group"}, {"pk": 3, "fields": {"permissions": []}, "model": "core.group"}]
[
{
"pk": 1,
"fields": {
"permissions": [
1, 2, 3, 7, 8, 9, 4, 5, 6, 10, 11, 12, 19, 20, 21, 22, 25, 23, 24, 16, 17, 18,
13, 14, 15
]
},
"model": "core.group"
},
{ "pk": 2, "fields": { "permissions": [25] }, "model": "core.group" },
{ "pk": 3, "fields": { "permissions": [] }, "model": "core.group" }
]

View File

@ -1 +1,74 @@
[{"pk": 1, "model": "core.page", "fields": {"full_name": "guy2", "owner_group": 1, "parent": null, "edit_groups": [], "name": "guy2", "view_groups": []}}, {"pk": 2, "model": "core.page", "fields": {"full_name": "guy2/bibou", "owner_group": 1, "parent": 1, "edit_group": [], "name": "bibou", "view_group": []}}, {"pk": 3, "model": "core.page", "fields": {"full_name": "guy2/bibou/troll", "owner_group": 1, "parent": 2, "edit_group": [], "name": "troll", "view_group": []}}, {"pk": 4, "model": "core.page", "fields": {"full_name": "guy", "owner_group": 1, "parent": null, "edit_group": [1], "name": "guy", "view_group": [1]}}, {"pk": 5, "model": "core.page", "fields": {"full_name": "bibou", "owner_group": 3, "parent": null, "edit_group": [1], "name": "bibou", "view_group": []}}, {"pk": 6, "model": "core.page", "fields": {"full_name": "guy2/guy", "owner_group": 1, "parent": 1, "edit_group": [], "name": "guy", "view_group": []}}]
[
{
"pk": 1,
"model": "core.page",
"fields": {
"full_name": "guy2",
"owner_group": 1,
"parent": null,
"edit_groups": [],
"name": "guy2",
"view_groups": []
}
},
{
"pk": 2,
"model": "core.page",
"fields": {
"full_name": "guy2/bibou",
"owner_group": 1,
"parent": 1,
"edit_group": [],
"name": "bibou",
"view_group": []
}
},
{
"pk": 3,
"model": "core.page",
"fields": {
"full_name": "guy2/bibou/troll",
"owner_group": 1,
"parent": 2,
"edit_group": [],
"name": "troll",
"view_group": []
}
},
{
"pk": 4,
"model": "core.page",
"fields": {
"full_name": "guy",
"owner_group": 1,
"parent": null,
"edit_group": [1],
"name": "guy",
"view_group": [1]
}
},
{
"pk": 5,
"model": "core.page",
"fields": {
"full_name": "bibou",
"owner_group": 3,
"parent": null,
"edit_group": [1],
"name": "bibou",
"view_group": []
}
},
{
"pk": 6,
"model": "core.page",
"fields": {
"full_name": "guy2/guy",
"owner_group": 1,
"parent": 1,
"edit_group": [],
"name": "guy",
"view_group": []
}
}
]

View File

@ -1 +1,42 @@
[{"model": "core.user", "pk": 1, "fields": {"first_name": "Ro", "date_joined": "2015-11-19T16:05:51.764Z", "groups": [], "password": "pbkdf2_sha256$20000$MDukCN5X8Bof$rYdhppKiusj+W/1Rxpy0yuYsEyWocESEjtRsopkOc5c=", "last_name": "Ot", "nick_name": "", "username": "root", "user_permissions": [], "email": "bibou@git.an", "last_login": "2015-11-26T16:28:36.464Z", "date_of_birth": "1969-12-31T23:00:00Z", "is_superuser": true, "is_active": true, "is_staff": true}}, {"model": "core.user", "pk": 2, "fields": {"first_name": "Skia", "date_joined": "2015-11-19T16:06:29.556Z", "groups": [3], "password": "pbkdf2_sha256$20000$UK9a29p5bBEh$Jzv7xs0W9njJZiXfIdYXDydim/3YHs6awKwDmN7gSAc=", "last_name": "Kia", "nick_name": "", "username": "skia", "user_permissions": [], "email": "plop@libskia.so", "last_login": "2015-11-26T16:37:01.671Z", "date_of_birth": "1969-12-31T23:00:00Z", "is_superuser": false, "is_active": true, "is_staff": false}}]
[
{
"model": "core.user",
"pk": 1,
"fields": {
"first_name": "Ro",
"date_joined": "2015-11-19T16:05:51.764Z",
"groups": [],
"password": "pbkdf2_sha256$20000$MDukCN5X8Bof$rYdhppKiusj+W/1Rxpy0yuYsEyWocESEjtRsopkOc5c=",
"last_name": "Ot",
"nick_name": "",
"username": "root",
"user_permissions": [],
"email": "bibou@git.an",
"last_login": "2015-11-26T16:28:36.464Z",
"date_of_birth": "1969-12-31T23:00:00Z",
"is_superuser": true,
"is_active": true,
"is_staff": true
}
},
{
"model": "core.user",
"pk": 2,
"fields": {
"first_name": "Skia",
"date_joined": "2015-11-19T16:06:29.556Z",
"groups": [3],
"password": "pbkdf2_sha256$20000$UK9a29p5bBEh$Jzv7xs0W9njJZiXfIdYXDydim/3YHs6awKwDmN7gSAc=",
"last_name": "Kia",
"nick_name": "",
"username": "skia",
"user_permissions": [],
"email": "plop@libskia.so",
"last_login": "2015-11-26T16:37:01.671Z",
"date_of_birth": "1969-12-31T23:00:00Z",
"is_superuser": false,
"is_active": true,
"is_staff": false
}
}
]

View File

@ -1,42 +1,121 @@
/*--------------------------------RESET--------------------------------*/
/*--------------------------------RESET--------------------------------*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
ol,
ul {
/* list-style: none;*/
}
blockquote, q {
blockquote,
q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {

View File

@ -1,59 +1,66 @@
$( function() {
buttons = $(".choose_file_button");
popups = $(".choose_file_widget");
$(() => {
// const buttons = $('.choose_file_button')
const popups = $(".choose_file_widget");
popups.dialog({
autoOpen: false,
modal: true,
width: "90%",
create: function (event) {
target = $(event.target);
create: (event) => {
const target = $(event.target);
target.parent().css({
'position': 'fixed',
'top': '5%',
'bottom': '5%',
position: "fixed",
top: "5%",
bottom: "5%",
});
target.css("height", "300px");
console.log(target);
},
buttons: [
{
text: "Choose",
click: function () {
console.log($("#file_id"));
$("input[name="+$(this).attr('name')+"]").attr('value', $("#file_id").attr('value'));
$(`input[name=${$(this).attr("name")}]`).attr(
"value",
$("#file_id").attr("value"),
);
$(this).dialog("close");
},
disabled: true,
}
},
],
});
$( ".choose_file_button" ).button().on( "click", function() {
popup = popups.filter("[name="+$(this).attr('name')+"]");
console.log(popup);
popup.html('<iframe src="/file/popup" width="100%" height="95%"></iframe><div id="file_id" value="null" />');
$(".choose_file_button")
.button()
.on("click", function () {
const popup = popups.filter(`[name=${$(this).attr("name")}]`);
popup.html(
'<iframe src="/file/popup" width="100%" height="95%"></iframe><div id="file_id" value="null" />',
);
popup.dialog({ title: $(this).text() }).dialog("open");
});
$("#quick_notif li").click(function () {
$(this).hide();
})
});
});
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function createQuickNotif(msg) {
const el = document.createElement('li')
el.textContent = msg
el.addEventListener('click', () => el.parentNode.removeChild(el))
document.getElementById('quick_notif').appendChild(el)
const el = document.createElement("li");
el.textContent = msg;
el.addEventListener("click", () => el.parentNode.removeChild(el));
document.getElementById("quick_notif").appendChild(el);
}
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function deleteQuickNotifs() {
const el = document.getElementById('quick_notif')
const el = document.getElementById("quick_notif");
while (el.firstChild) {
el.removeChild(el.firstChild)
el.removeChild(el.firstChild);
}
}
function display_notif() {
$('#header_notif').toggle().parent().toggleClass("white");
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function displayNotif() {
$("#header_notif").toggle().parent().toggleClass("white");
}
// You can't get the csrf token from the template in a widget
@ -62,11 +69,13 @@ function display_notif() {
// Sadly, getting the cookie is not possible with CSRF_COOKIE_HTTPONLY or CSRF_USE_SESSIONS is True
// So, the true workaround is to get the token from the dom
// https://docs.djangoproject.com/en/2.0/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-is-true
// biome-ignore lint/style/useNamingConvention: can't find it used anywhere but I will not play with the devil
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function getCSRFToken() {
return $("[name=csrfmiddlewaretoken]").val();
}
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
const initialUrlParams = new URLSearchParams(window.location.search);
/**
@ -74,8 +83,11 @@ const initialUrlParams = new URLSearchParams(window.location.search);
* @enum {number}
*/
const History = {
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
NONE: 0,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
PUSH: 1,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
REPLACE: 2,
};
@ -85,27 +97,31 @@ const History = {
* @param {History} action
* @param {URL | null} url
*/
function update_query_string(key, value, action = History.REPLACE, url = null) {
if (!url){
url = new URL(window.location.href);
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function updateQueryString(key, value, action = History.REPLACE, url = null) {
let ret = url;
if (!ret) {
ret = new URL(window.location.href);
}
if (value === undefined || value === null || value === "") {
// If the value is null, undefined or empty => delete it
url.searchParams.delete(key)
ret.searchParams.delete(key);
} else if (Array.isArray(value)) {
url.searchParams.delete(key)
value.forEach((v) => url.searchParams.append(key, v))
ret.searchParams.delete(key);
for (const v of value) {
ret.searchParams.append(key, v);
}
} else {
url.searchParams.set(key, value);
ret.searchParams.set(key, value);
}
if (action === History.PUSH) {
history.pushState(null, "", url.toString());
window.history.pushState(null, "", ret.toString());
} else if (action === History.REPLACE) {
history.replaceState(null, "", url.toString());
window.history.replaceState(null, "", ret.toString());
}
return url;
return ret;
}
// TODO : If one day a test workflow is made for JS in this project
@ -116,27 +132,28 @@ function update_query_string(key, value, action = History.REPLACE, url = null) {
* @param {string} url The paginated endpoint to fetch
* @return {Promise<Object[]>}
*/
async function fetch_paginated(url) {
const max_per_page = 199;
const paginated_url = new URL(url, document.location.origin);
paginated_url.searchParams.set("page_size", max_per_page.toString());
paginated_url.searchParams.set("page", "1");
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
async function fetchPaginated(url) {
const maxPerPage = 199;
const paginatedUrl = new URL(url, document.location.origin);
paginatedUrl.searchParams.set("page_size", maxPerPage.toString());
paginatedUrl.searchParams.set("page", "1");
let first_page = (await ( await fetch(paginated_url)).json());
let results = first_page.results;
const firstPage = await (await fetch(paginatedUrl)).json();
const results = firstPage.results;
const nb_pictures = first_page.count
const nb_pages = Math.ceil(nb_pictures / max_per_page);
const nbPictures = firstPage.count;
const nbPages = Math.ceil(nbPictures / maxPerPage);
if (nb_pages > 1) {
let promises = [];
for (let i = 2; i <= nb_pages; i++) {
paginated_url.searchParams.set("page", i.toString());
if (nbPages > 1) {
const promises = [];
for (let i = 2; i <= nbPages; i++) {
paginatedUrl.searchParams.set("page", i.toString());
promises.push(
fetch(paginated_url).then(res => res.json().then(json => json.results))
fetch(paginatedUrl).then((res) => res.json().then((json) => json.results)),
);
}
results.push(...(await Promise.all(promises)).flat())
results.push(...(await Promise.all(promises)).flat());
}
return results;
}

View File

@ -19,4 +19,106 @@
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
!function(e){e.fn.shorten=function(s){"use strict";var t={showChars:100,minHideChars:10,ellipsesText:"...",moreText:"more",lessText:"less",onLess:function(){},onMore:function(){},errMsg:null,force:!1};return s&&e.extend(t,s),(!e(this).data("jquery.shorten")||!!t.force)&&(e(this).data("jquery.shorten",!0),e(document).off("click",".morelink"),e(document).on({click:function(){var s=e(this);return s.hasClass("less")?(s.removeClass("less"),s.html(t.moreText),s.parent().prev().animate({},function(){s.parent().prev().prev().show()}).hide("fast",function(){t.onLess()})):(s.addClass("less"),s.html(t.lessText),s.parent().prev().animate({},function(){s.parent().prev().prev().hide()}).show("fast",function(){t.onMore()})),!1}},".morelink"),this.each(function(){var s=e(this),n=s.html();if(s.text().length>t.showChars+t.minHideChars){var r=n.substr(0,t.showChars);if(r.indexOf("<")>=0){for(var a=!1,o="",i=0,l=[],h=null,c=0,f=0;f<=t.showChars;c++)if("<"!=n[c]||a||(a=!0,"/"==(h=n.substring(c+1,n.indexOf(">",c)))[0]?h!="/"+l[0]?t.errMsg="ERROR en HTML: the top of the stack should be the tag that closes":l.shift():"br"!=h.toLowerCase()&&l.unshift(h)),a&&">"==n[c]&&(a=!1),a)o+=n.charAt(c);else if(f++,i<=t.showChars)o+=n.charAt(c),i++;else if(l.length>0){for(j=0;j<l.length;j++)o+="</"+l[j]+">";break}r=e("<div/>").html(o+'<span class="ellip">'+t.ellipsesText+"</span>").html()}else r+=t.ellipsesText;var p='<div class="shortcontent">'+r+'</div><div class="allcontent">'+n+'</div><span><a href="javascript://nop/" class="morelink">'+t.moreText+"</a></span>";s.html(p),s.find(".allcontent").hide(),e(".shortcontent p:last",s).css("margin-bottom",0)}}))}}(jQuery);
!(function (e) {
e.fn.shorten = function (s) {
"use strict";
var t = {
showChars: 100,
minHideChars: 10,
ellipsesText: "...",
moreText: "more",
lessText: "less",
onLess: function () {},
onMore: function () {},
errMsg: null,
force: !1,
};
return (
s && e.extend(t, s),
(!e(this).data("jquery.shorten") || !!t.force) &&
(e(this).data("jquery.shorten", !0),
e(document).off("click", ".morelink"),
e(document).on(
{
click: function () {
var s = e(this);
return (
s.hasClass("less")
? (s.removeClass("less"),
s.html(t.moreText),
s
.parent()
.prev()
.animate({}, function () {
s.parent().prev().prev().show();
})
.hide("fast", function () {
t.onLess();
}))
: (s.addClass("less"),
s.html(t.lessText),
s
.parent()
.prev()
.animate({}, function () {
s.parent().prev().prev().hide();
})
.show("fast", function () {
t.onMore();
})),
!1
);
},
},
".morelink",
),
this.each(function () {
var s = e(this),
n = s.html();
if (s.text().length > t.showChars + t.minHideChars) {
var r = n.substr(0, t.showChars);
if (r.indexOf("<") >= 0) {
for (
var a = !1, o = "", i = 0, l = [], h = null, c = 0, f = 0;
f <= t.showChars;
c++
)
if (
("<" != n[c] ||
a ||
((a = !0),
"/" == (h = n.substring(c + 1, n.indexOf(">", c)))[0]
? h != "/" + l[0]
? (t.errMsg =
"ERROR en HTML: the top of the stack should be the tag that closes")
: l.shift()
: "br" != h.toLowerCase() && l.unshift(h)),
a && ">" == n[c] && (a = !1),
a)
)
o += n.charAt(c);
else if ((f++, i <= t.showChars)) (o += n.charAt(c)), i++;
else if (l.length > 0) {
for (j = 0; j < l.length; j++) o += "</" + l[j] + ">";
break;
}
r = e("<div/>")
.html(o + '<span class="ellip">' + t.ellipsesText + "</span>")
.html();
} else r += t.ellipsesText;
var p =
'<div class="shortcontent">' +
r +
'</div><div class="allcontent">' +
n +
'</div><span><a href="javascript://nop/" class="morelink">' +
t.moreText +
"</a></span>";
s.html(p),
s.find(".allcontent").hide(),
e(".shortcontent p:last", s).css("margin-bottom", 0);
}
}))
);
};
})(jQuery);

View File

@ -15,7 +15,7 @@
* ];
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"),
* data_source: local_data_source(data)
* dataSource: localDataSource(data)
* }));
* ```
*
@ -29,7 +29,7 @@
* ];
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"),
* data_source: local_data_source(data, {
* dataSource: localDataSource(data, {
* excluded: () => data.filter((i) => i.text === "to exclude").map((i) => parseInt(i))
* })
* }));
@ -38,15 +38,15 @@
* # Remote data source
*
* Select2 with remote data sources are similar to those with local
* data, but with some more parameters, like `result_converter`,
* data, but with some more parameters, like `resultConverter`,
* which takes a callback that must return a `Select2Object`.
*
* ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"),
* data_source: remote_data_source("/api/user/search", {
* dataSource: remoteDataSource("/api/user/search", {
* excluded: () => [1, 2], // exclude users 1 and 2 from the search
* result_converter: (user) => Object({id: user.id, text: user.first_name})
* resultConverter: (user) => Object({id: user.id, text: user.firstName})
* })
* }));
* ```
@ -62,8 +62,8 @@
* ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"),
* data_source: remote_data_source("/api/user/search", {
* result_converter: (user) => Object({id: user.id, text: user.first_name}),
* dataSource: remoteDataSource("/api/user/search", {
* resultConverter: (user) => Object({id: user.id, text: user.firstName}),
* overrides: {
* delay: 500
* }
@ -85,15 +85,15 @@
*
* Sometimes, you would like to display an image besides
* the text on the select items.
* In this case, fill the `picture_getter` option :
* In this case, fill the `pictureGetter` option :
*
* ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"),
* data_source: remote_data_source("/api/user/search", {
* result_converter: (user) => Object({id: user.id, text: user.first_name})
* dataSource: remoteDataSource("/api/user/search", {
* resultConverter: (user) => Object({id: user.id, text: user.firstName})
* })
* picture_getter: (user) => user.profile_pict,
* pictureGetter: (user) => user.profilePict,
* }));
* ```
*
@ -105,8 +105,8 @@
* <body>
* <div x-data="select2_test">
* <select x-ref="search" x-ref="select"></select>
* <p x-text="current_selection.id"></p>
* <p x-text="current_selection.text"></p>
* <p x-text="currentSelection.id"></p>
* <p x-text="currentSelection.text"></p>
* </div>
* </body>
*
@ -114,20 +114,20 @@
* document.addEventListener("alpine:init", () => {
* Alpine.data("select2_test", () => ({
* selector: undefined,
* current_select: {id: "", text: ""},
* currentSelect: {id: "", text: ""},
*
* init() {
* this.selector = sithSelect2({
* element: $(this.$refs.select),
* data_source: local_data_source(
* dataSource: localDataSource(
* [{id: 1, text: "foo"}, {id: 2, text: "bar"}]
* ),
* });
* this.selector.on("select2:select", (event) => {
* // select2 => Alpine signals here
* this.current_select = this.selector.select2("data")
* this.currentSelect = this.selector.select2("data")
* });
* this.$watch("current_selected" (value) => {
* this.$watch("currentSelected" (value) => {
* // Alpine => select2 signals here
* });
* },
@ -145,10 +145,10 @@
/**
* @typedef Select2Options
* @property {Element} element
* @property {Object} data_source
* the data source, built with `local_data_source` or `remote_data_source`
* @property {Object} dataSource
* the data source, built with `localDataSource` or `remoteDataSource`
* @property {number[]} excluded A list of ids to exclude from search
* @property {undefined | function(Object): string} picture_getter
* @property {undefined | function(Object): string} pictureGetter
* A callback to get the picture field from the API response
* @property {Object | undefined} overrides
* Any other select2 parameter to apply on the config
@ -157,13 +157,14 @@
/**
* @param {Select2Options} options
*/
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function sithSelect2(options) {
const elem = $(options.element);
return elem.select2({
theme: elem[0].multiple ? "classic" : "default",
minimumInputLength: 2,
templateResult: select_item_builder(options.picture_getter),
...options.data_source,
templateResult: selectItemBuilder(options.pictureGetter),
...options.dataSource,
...(options.overrides || {}),
});
}
@ -179,8 +180,9 @@ function sithSelect2(options) {
* @param {Select2Object[]} source The array containing the data
* @param {RemoteSourceOptions} options
*/
function local_data_source(source, options) {
if (!!options.excluded) {
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function localDataSource(source, options) {
if (options.excluded) {
const ids = options.excluded();
return { data: source.filter((i) => !ids.includes(i.id)) };
}
@ -191,7 +193,7 @@ function local_data_source(source, options) {
* @typedef RemoteSourceOptions
* @property {undefined | function(): number[]} excluded
* A callback to the ids to exclude from the search
* @property {undefined | function(): Select2Object} result_converter
* @property {undefined | function(): Select2Object} resultConverter
* A converter for a value coming from the remote api
* @property {undefined | Object} overrides
* Any other select2 parameter to apply on the config
@ -202,9 +204,11 @@ function local_data_source(source, options) {
* @param {string} source The url of the endpoint
* @param {RemoteSourceOptions} options
*/
function remote_data_source(source, options) {
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function remoteDataSource(source, options) {
jQuery.ajaxSettings.traditional = true;
let params = {
const params = {
url: source,
dataType: "json",
cache: true,
@ -213,24 +217,25 @@ function remote_data_source(source, options) {
return {
search: params.term,
exclude: [
...(this.val() || []).map((i) => parseInt(i)),
...(this.val() || []).map((i) => Number.parseInt(i)),
...(options.excluded ? options.excluded() : []),
],
};
},
};
if (!!options.result_converter) {
params["processResults"] = function (data) {
return { results: data.results.map(options.result_converter) };
};
if (options.resultConverter) {
params.processResults = (data) => ({
results: data.results.map(options.resultConverter),
});
}
if (!!options.overrides) {
if (options.overrides) {
Object.assign(params, options.overrides);
}
return { ajax: params };
}
function item_formatter(user) {
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function itemFormatter(user) {
if (user.loading) {
return user.text;
}
@ -238,23 +243,22 @@ function item_formatter(user) {
/**
* Build a function to display the results
* @param {null | function(Object):string} picture_getter
* @param {null | function(Object):string} pictureGetter
* @return {function(string): jQuery|HTMLElement}
*/
function select_item_builder(picture_getter) {
function selectItemBuilder(pictureGetter) {
return (item) => {
const picture =
typeof picture_getter === "function" ? picture_getter(item) : null;
const img_html = picture
const picture = typeof pictureGetter === "function" ? pictureGetter(item) : null;
const imgHtml = picture
? `<img
src="${picture_getter(item)}"
src="${pictureGetter(item)}"
alt="${item.text}"
onerror="this.src = '/static/core/img/unknown.jpg'"
/>`
: "";
return $(`<div class="select-item">
${img_html}
${imgHtml}
<span class="select-item-text">${item.text}</span>
</div>`);
};

View File

@ -1,7 +1,7 @@
async function get_graph_data(url, godfathers_depth, godchildren_depth) {
let data = await (
async function getGraphData(url, godfathersDepth, godchildrenDepth) {
const data = await (
await fetch(
`${url}?godfathers_depth=${godfathers_depth}&godchildren_depth=${godchildren_depth}`,
`${url}?godfathers_depth=${godfathersDepth}&godchildren_depth=${godchildrenDepth}`,
)
).json();
return [
@ -16,12 +16,13 @@ async function get_graph_data(url, godfathers_depth, godchildren_depth) {
];
}
function create_graph(container, data, active_user_id) {
let cy = cytoscape({
function createGraph(container, data, activeUserId) {
// biome-ignore lint/correctness/noUndeclaredVariables: imported by user_godphaters_tree.jinja
const cy = cytoscape({
boxSelectionEnabled: false,
autounselectify: true,
container: container,
container,
elements: data,
minZoom: 0.5,
@ -83,11 +84,9 @@ function create_graph(container, data, active_user_id) {
},
},
});
let active_user = cy
.getElementById(active_user_id)
.style("shape", "rectangle");
const activeUser = cy.getElementById(activeUserId).style("shape", "rectangle");
/* Reset graph */
let reset_graph = () => {
const resetGraph = () => {
cy.elements((element) => {
if (element.hasClass("traversed")) {
element.removeClass("traversed");
@ -98,38 +97,33 @@ function create_graph(container, data, active_user_id) {
});
};
let on_node_tap = (el) => {
reset_graph();
const onNodeTap = (el) => {
resetGraph();
/* Create path on graph if selected isn't the targeted user */
if (el === active_user) {
if (el === activeUser) {
return;
}
cy.elements((element) => {
element.addClass("not-traversed");
});
cy.elements()
.aStar({
for (const traversed of cy.elements().aStar({
root: el,
goal: active_user,
})
.path.forEach((el) => {
el.removeClass("not-traversed");
el.addClass("traversed");
});
goal: activeUser,
}).path) {
traversed.removeClass("not-traversed");
traversed.addClass("traversed");
}
};
cy.on("tap", "node", (tapped) => {
on_node_tap(tapped.target);
onNodeTap(tapped.target);
});
cy.zoomingEnabled(false);
/* Add context menu */
if (cy.cxtmenu === undefined) {
console.error(
"ctxmenu isn't loaded, context menu won't be available on graphs",
);
return cy;
throw new Error("ctxmenu isn't loaded, context menu won't be available on graphs");
}
cy.cxtmenu({
selector: "node",
@ -137,22 +131,22 @@ function create_graph(container, data, active_user_id) {
commands: [
{
content: '<i class="fa fa-external-link fa-2x"></i>',
select: function (el) {
select: (el) => {
window.open(el.data().profile_url, "_blank").focus();
},
},
{
content: '<span class="fa fa-mouse-pointer fa-2x"></span>',
select: function (el) {
on_node_tap(el);
select: (el) => {
onNodeTap(el);
},
},
{
content: '<i class="fa fa-eraser fa-2x"></i>',
select: function (el) {
reset_graph();
select: (_) => {
resetGraph();
},
},
],
@ -165,68 +159,77 @@ document.addEventListener("alpine:init", () => {
/*
This needs some constants to be set before the document has been loaded
api_url: base url for fetching the tree as a string
active_user: id of the user to fetch the tree from
depth_min: minimum tree depth for godfathers and godchildren as an int
depth_max: maximum tree depth for godfathers and godchildren as an int
apiUrl: base url for fetching the tree as a string
activeUser: id of the user to fetch the tree from
depthMin: minimum tree depth for godfathers and godchildren as an int
depthMax: maximum tree depth for godfathers and godchildren as an int
*/
const default_depth = 2;
const defaultDepth = 2;
if (
typeof api_url === "undefined" ||
typeof active_user === "undefined" ||
typeof depth_min === "undefined" ||
typeof depth_max === "undefined"
// biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
typeof apiUrl === "undefined" ||
// biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
typeof activeUser === "undefined" ||
// biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
typeof depthMin === "undefined" ||
// biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
typeof depthMax === "undefined"
) {
console.error(
throw new Error(
"Some constants are not set before using the family_graph script, please look at the documentation",
);
return;
}
function get_initial_depth(prop) {
let value = parseInt(initialUrlParams.get(prop));
if (isNaN(value) || value < depth_min || value > depth_max) {
return default_depth;
function getInitialDepth(prop) {
// biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
const value = Number.parseInt(initialUrlParams.get(prop));
// biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
if (Number.isNaN(value) || value < depthMin || value > depthMax) {
return defaultDepth;
}
return value;
}
Alpine.data("graph", () => ({
loading: false,
godfathers_depth: get_initial_depth("godfathers_depth"),
godchildren_depth: get_initial_depth("godchildren_depth"),
godfathersDepth: getInitialDepth("godfathersDepth"),
godchildrenDepth: getInitialDepth("godchildrenDepth"),
// biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
reverse: initialUrlParams.get("reverse")?.toLowerCase?.() === "true",
graph: undefined,
graph_data: {},
graphData: {},
async init() {
let delayed_fetch = Alpine.debounce(async () => {
this.fetch_graph_data();
const delayedFetch = Alpine.debounce(async () => {
await this.fetchGraphData();
}, 100);
["godfathers_depth", "godchildren_depth"].forEach((param) => {
for (const param of ["godfathersDepth", "godchildrenDepth"]) {
this.$watch(param, async (value) => {
if (value < depth_min || value > depth_max) {
// biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
if (value < depthMin || value > depthMax) {
return;
}
update_query_string(param, value, History.REPLACE);
delayed_fetch();
});
// biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
updateQueryString(param, value, History.REPLACE);
await delayedFetch();
});
}
this.$watch("reverse", async (value) => {
update_query_string("reverse", value, History.REPLACE);
this.reverse_graph();
// biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
updateQueryString("reverse", value, History.REPLACE);
await this.reverseGraph();
});
this.$watch("graph_data", async () => {
await this.generate_graph();
this.$watch("graphData", async () => {
await this.generateGraph();
if (this.reverse) {
await this.reverse_graph();
await this.reverseGraph();
}
});
this.fetch_graph_data();
await this.fetchGraphData();
},
async screenshot() {
screenshot() {
const link = document.createElement("a");
link.href = this.graph.jpg();
link.download = interpolate(
@ -239,34 +242,32 @@ document.addEventListener("alpine:init", () => {
document.body.removeChild(link);
},
async reset() {
reset() {
this.reverse = false;
this.godfathers_depth = default_depth;
this.godchildren_depth = default_depth;
this.godfathersDepth = defaultDepth;
this.godchildrenDepth = defaultDepth;
},
async reverse_graph() {
async reverseGraph() {
this.graph.elements((el) => {
el.position(new Object({ x: -el.position().x, y: -el.position().y }));
el.position({ x: -el.position().x, y: -el.position().y });
});
this.graph.center(this.graph.elements());
},
async fetch_graph_data() {
this.graph_data = await get_graph_data(
api_url,
this.godfathers_depth,
this.godchildren_depth,
async fetchGraphData() {
this.graphData = await getGraphData(
// biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
apiUrl,
this.godfathersDepth,
this.godchildrenDepth,
);
},
async generate_graph() {
async generateGraph() {
this.loading = true;
this.graph = create_graph(
$(this.$refs.graph),
this.graph_data,
active_user,
);
// biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
this.graph = await createGraph($(this.$refs.graph), this.graphData, activeUser);
this.loading = false;
},
}));

View File

@ -1,28 +1,25 @@
function alpine_webcam_builder(
default_picture,
delete_url,
can_delete_picture,
) {
// biome-ignore lint/correctness/noUnusedVariables: used in user_edit.jinja
function alpineWebcamBuilder(defaultPicture, deleteUrl, canDeletePicture) {
return () => ({
can_edit_picture: false,
canEditPicture: false,
loading: false,
is_camera_enabled: false,
is_camera_error: false,
isCameraEnabled: false,
isCameraError: false,
picture: null,
video: null,
picture_form: null,
pictureForm: null,
init() {
this.video = this.$refs.video;
this.picture_form = this.$refs.form.getElementsByTagName("input");
if (this.picture_form.length > 0) {
this.picture_form = this.picture_form[0];
this.can_edit_picture = true;
this.pictureForm = this.$refs.form.getElementsByTagName("input");
if (this.pictureForm.length > 0) {
this.pictureForm = this.pictureForm[0];
this.canEditPicture = true;
// Link the displayed element to the form input
this.picture_form.onchange = (event) => {
let files = event.srcElement.files;
this.pictureForm.onchange = (event) => {
const files = event.srcElement.files;
if (files.length > 0) {
this.picture = (window.URL || window.webkitURL).createObjectURL(
event.srcElement.files[0],
@ -34,77 +31,78 @@ function alpine_webcam_builder(
}
},
get_picture() {
return this.picture || default_picture;
getPicture() {
return this.picture || defaultPicture;
},
delete_picture() {
deletePicture() {
// Only remove currently displayed picture
if (!!this.picture) {
let list = new DataTransfer();
this.picture_form.files = list.files;
this.picture_form.dispatchEvent(new Event("change"));
if (this.picture) {
const list = new DataTransfer();
this.pictureForm.files = list.files;
this.pictureForm.dispatchEvent(new Event("change"));
return;
}
if (!can_delete_picture) {
if (!canDeletePicture) {
return;
}
// Remove user picture if correct rights are available
window.open(delete_url, "_self");
window.open(deleteUrl, "_self");
},
enable_camera() {
enableCamera() {
this.picture = null;
this.loading = true;
this.is_camera_error = false;
this.isCameraError = false;
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
this.loading = false;
this.is_camera_enabled = true;
this.isCameraEnabled = true;
this.video.srcObject = stream;
this.video.play();
})
.catch((err) => {
this.is_camera_error = true;
this.isCameraError = true;
this.loading = false;
throw err;
});
},
take_picture() {
let canvas = document.createElement("canvas");
takePicture() {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
/* Create the image */
let settings = this.video.srcObject.getTracks()[0].getSettings();
const settings = this.video.srcObject.getTracks()[0].getSettings();
canvas.width = settings.width;
canvas.height = settings.height;
context.drawImage(this.video, 0, 0, canvas.width, canvas.height);
/* Stop camera */
this.video.pause();
this.video.srcObject.getTracks().forEach((track) => {
for (const track of this.video.srcObject.getTracks()) {
if (track.readyState === "live") {
track.stop();
}
});
}
canvas.toBlob((blob) => {
const filename = interpolate(gettext("captured.%s"), ["webp"]);
let file = new File([blob], filename, {
const file = new File([blob], filename, {
type: "image/webp",
});
let list = new DataTransfer();
const list = new DataTransfer();
list.items.add(file);
this.picture_form.files = list.files;
this.pictureForm.files = list.files;
// No change event is triggered, we trigger it manually #}
this.picture_form.dispatchEvent(new Event("change"));
this.pictureForm.dispatchEvent(new Event("change"));
}, "image/webp");
canvas.remove();
this.is_camera_enabled = false;
this.isCameraEnabled = false;
},
});
}

View File

@ -2,6 +2,6 @@ import Alpine from "alpinejs";
window.Alpine = Alpine;
addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("DOMContentLoaded", () => {
Alpine.start();
});

View File

@ -1,6 +1,7 @@
// biome-ignore lint/correctness/noUndeclaredDependencies: shipped by easymde
import "codemirror/lib/codemirror.css";
import "easymde/src/css/easymde.css";
import EasyMDE from "easymde";
import easyMde from "easymde";
// This scripts dependens on Alpine but it should be loaded on every page
@ -9,13 +10,13 @@ import EasyMDE from "easymde";
* @param {HTMLTextAreaElement} textarea to use
* @param {string} link to the markdown api
**/
function easymde_factory (textarea, markdown_api_url) {
const easymde = new EasyMDE({
function easymdeFactory(textarea, markdownApiUrl) {
const easymde = new easyMde({
element: textarea,
spellChecker: false,
autoDownloadFontAwesome: false,
previewRender: Alpine.debounce(async (plainText, preview) => {
const res = await fetch(markdown_api_url, {
const res = await fetch(markdownApiUrl, {
method: "POST",
body: JSON.stringify({ text: plainText }),
});
@ -26,33 +27,33 @@ function easymde_factory (textarea, markdown_api_url) {
toolbar: [
{
name: "heading-smaller",
action: EasyMDE.toggleHeadingSmaller,
action: easyMde.toggleHeadingSmaller,
className: "fa fa-header",
title: gettext("Heading"),
},
{
name: "italic",
action: EasyMDE.toggleItalic,
action: easyMde.toggleItalic,
className: "fa fa-italic",
title: gettext("Italic"),
},
{
name: "bold",
action: EasyMDE.toggleBold,
action: easyMde.toggleBold,
className: "fa fa-bold",
title: gettext("Bold"),
},
{
name: "strikethrough",
action: EasyMDE.toggleStrikethrough,
action: easyMde.toggleStrikethrough,
className: "fa fa-strikethrough",
title: gettext("Strikethrough"),
},
{
name: "underline",
action: function customFunction(editor) {
let cm = editor.codemirror;
cm.replaceSelection("__" + cm.getSelection() + "__");
const cm = editor.codemirror;
cm.replaceSelection(`__${cm.getSelection()}__`);
},
className: "fa fa-underline",
title: gettext("Underline"),
@ -60,8 +61,8 @@ function easymde_factory (textarea, markdown_api_url) {
{
name: "superscript",
action: function customFunction(editor) {
let cm = editor.codemirror;
cm.replaceSelection("^" + cm.getSelection() + "^");
const cm = editor.codemirror;
cm.replaceSelection(`^${cm.getSelection()}^`);
},
className: "fa fa-superscript",
title: gettext("Superscript"),
@ -69,80 +70,80 @@ function easymde_factory (textarea, markdown_api_url) {
{
name: "subscript",
action: function customFunction(editor) {
let cm = editor.codemirror;
cm.replaceSelection("~" + cm.getSelection() + "~");
const cm = editor.codemirror;
cm.replaceSelection(`~${cm.getSelection()}~`);
},
className: "fa fa-subscript",
title: gettext("Subscript"),
},
{
name: "code",
action: EasyMDE.toggleCodeBlock,
action: easyMde.toggleCodeBlock,
className: "fa fa-code",
title: gettext("Code"),
},
"|",
{
name: "quote",
action: EasyMDE.toggleBlockquote,
action: easyMde.toggleBlockquote,
className: "fa fa-quote-left",
title: gettext("Quote"),
},
{
name: "unordered-list",
action: EasyMDE.toggleUnorderedList,
action: easyMde.toggleUnorderedList,
className: "fa fa-list-ul",
title: gettext("Unordered list"),
},
{
name: "ordered-list",
action: EasyMDE.toggleOrderedList,
action: easyMde.toggleOrderedList,
className: "fa fa-list-ol",
title: gettext("Ordered list"),
},
"|",
{
name: "link",
action: EasyMDE.drawLink,
action: easyMde.drawLink,
className: "fa fa-link",
title: gettext("Insert link"),
},
{
name: "image",
action: EasyMDE.drawImage,
action: easyMde.drawImage,
className: "fa-regular fa-image",
title: gettext("Insert image"),
},
{
name: "table",
action: EasyMDE.drawTable,
action: easyMde.drawTable,
className: "fa fa-table",
title: gettext("Insert table"),
},
"|",
{
name: "clean-block",
action: EasyMDE.cleanBlock,
action: easyMde.cleanBlock,
className: "fa fa-eraser fa-clean-block",
title: gettext("Clean block"),
},
"|",
{
name: "preview",
action: EasyMDE.togglePreview,
action: easyMde.togglePreview,
className: "fa fa-eye no-disable",
title: gettext("Toggle preview"),
},
{
name: "side-by-side",
action: EasyMDE.toggleSideBySide,
action: easyMde.toggleSideBySide,
className: "fa fa-columns no-disable no-mobile",
title: gettext("Toggle side by side"),
},
{
name: "fullscreen",
action: EasyMDE.toggleFullScreen,
className: "fa fa-arrows-alt no-disable no-mobile",
action: easyMde.toggleFullScreen,
className: "fa fa-expand no-mobile",
title: gettext("Toggle fullscreen"),
},
"|",
@ -155,18 +156,16 @@ function easymde_factory (textarea, markdown_api_url) {
],
});
const submits = textarea
.closest("form")
.querySelectorAll('input[type="submit"]');
const submits = textarea.closest("form").querySelectorAll('input[type="submit"]');
const parentDiv = textarea.parentElement;
let submitPressed = false;
function checkMarkdownInput(e) {
function checkMarkdownInput() {
// an attribute is null if it does not exist, else a string
const required = textarea.getAttribute("required") != null;
const length = textarea.value.trim().length;
if (required && length == 0) {
if (required && length === 0) {
parentDiv.style.boxShadow = "red 0px 0px 1.5px 1px";
} else {
parentDiv.style.boxShadow = "";
@ -181,9 +180,9 @@ function easymde_factory (textarea, markdown_api_url) {
checkMarkdownInput(e);
}
submits.forEach((submit) => {
for (const submit of submits) {
submit.addEventListener("click", onSubmitClick);
});
};
}
}
window.easymde_factory = easymde_factory;
window.easymdeFactory = easymdeFactory;

View File

@ -18,7 +18,7 @@ require("jquery-ui/themes/base/all.css");
* @param {string} selector to be passed to jQuery
* @param {Object} options object to pass to the shorten function
**/
export function shorten(selector, options) {
function shorten(selector, options) {
$(selector).shorten(options);
}

View File

@ -112,7 +112,7 @@
></a>
</div>
<div class="notification">
<a href="#" onclick="display_notif()">
<a href="#" onclick="displayNotif()">
<i class="fa-regular fa-bell"></i>
{% set notification_count = user.notifications.filter(viewed=False).count() %}

View File

@ -15,36 +15,36 @@
{% macro profile_picture(field_name) %}
{% set this_picture = form.instance[field_name] %}
<div class="profile-picture" x-data="camera_{{ field_name }}" >
<div class="profile-picture-display" :aria-busy="loading" :class="{ 'camera-error': is_camera_error }">
<div class="profile-picture-display" :aria-busy="loading" :class="{ 'camera-error': isCameraError }">
<img
x-show="!is_camera_enabled && !is_camera_error"
:src="get_picture()"
x-show="!isCameraEnabled && !isCameraError"
:src="getPicture()"
alt="{%- trans -%}Profile{%- endtrans -%}" title="{%- trans -%}Profile{%- endtrans -%}"
loading="lazy"
/>
<video
x-show="is_camera_enabled"
x-show="isCameraEnabled"
x-ref="video"
></video>
<i
x-show="is_camera_error"
x-show="isCameraError"
x-cloak
class="fa fa-eye-slash"
></i>
</div>
<div class="profile-picture-buttons" x-show="can_edit_picture">
<div class="profile-picture-buttons" x-show="canEditPicture">
<button
x-show="can_edit_picture && !is_camera_enabled"
x-show="canEditPicture && !isCameraEnabled"
class="btn btn-blue"
@click.prevent="enable_camera()"
@click.prevent="enableCamera()"
>
<i class="fa fa-camera"></i>
{% trans %}Enable camera{% endtrans %}
</button>
<button
x-show="is_camera_enabled"
x-show="isCameraEnabled"
class="btn btn-blue"
@click.prevent="take_picture()"
@click.prevent="takePicture()"
>
<i class="fa fa-camera"></i>
{% trans %}Take a picture{% endtrans %}
@ -54,7 +54,7 @@
{%- if form[field_name] -%}
<div>
{{ form[field_name] }}
<button class="btn btn-red" @click.prevent="delete_picture()"
<button class="btn btn-red" @click.prevent="deletePicture()"
{%- if not (this_picture and this_picture.is_owned_by(user)) -%}
:disabled="!picture"
{%- endif -%}
@ -86,7 +86,7 @@
document.addEventListener("alpine:init", () => {
Alpine.data(
"camera_{{ field_name }}",
alpine_webcam_builder(
alpineWebcamBuilder(
{{ default_picture }},
{{ delete_url }},
{{ (this_picture and this_picture.is_owned_by(user))|tojson }}

View File

@ -30,21 +30,21 @@
</label>
<span class="depth-choice">
<button
@click="godfathers_depth--"
:disabled="godfathers_depth <= {{ depth_min }}"
@click="godfathersDepth--"
:disabled="godfathersDepth <= {{ depth_min }}"
><i class="fa fa-minus"></i></button>
<input
x-model="godfathers_depth"
x-model="godfathersDepth"
x-ref="godfather_depth_input"
type="number"
name="godfathers_depth"
name="godfathersDepth"
id="godfather-depth-input"
min="{{ depth_min }}"
max="{{ depth_max }}"
/>
<button
@click="godfathers_depth++"
:disabled="godfathers_depth >= {{ depth_max }}"
@click="godfathersDepth++"
:disabled="godfathersDepth >= {{ depth_max }}"
><i class="fa fa-plus"
></i></button>
</span>
@ -56,22 +56,22 @@
</label>
<span class="depth-choice">
<button
@click="godchildren_depth--"
:disabled="godchildren_depth <= {{ depth_min }}"
@click="godchildrenDepth--"
:disabled="godchildrenDepth <= {{ depth_min }}"
><i
class="fa fa-minus"
></i></button>
<input
x-model="godchildren_depth"
x-model="godchildrenDepth"
type="number"
name="godchildren_depth"
name="godchildrenDepth"
id="godchild-depth-input"
min="{{ depth_min }}"
max="{{ depth_max }}"
/>
<button
@click="godchildren_depth++"
:disabled="godchildren_depth >= {{ depth_max }}"
@click="godchildrenDepth++"
:disabled="godchildrenDepth >= {{ depth_max }}"
><i class="fa fa-plus"
></i></button>
</span>
@ -96,10 +96,10 @@
</div>
<script>
const api_url = "{{ api_url }}";
const active_user = "{{ object.id }}"
const depth_min = {{ depth_min }};
const depth_max = {{ depth_max }};
const apiUrl = "{{ api_url }}";
const activeUser = "{{ object.id }}"
const depthMin = {{ depth_min }};
const depthMax = {{ depth_max }};
</script>
{% endblock %}

View File

@ -21,14 +21,14 @@
{% if user.id == object.id %}
<div x-show="pictures.length > 0" x-cloak>
<button
:disabled="is_downloading"
:disabled="isDownloading"
class="btn btn-blue"
@click="download_zip()"
@click="downloadZip()"
>
<i class="fa fa-download"></i>
{% trans %}Download all my pictures{% endtrans %}
</button>
<progress x-ref="progress" x-show="is_downloading"></progress>
<progress x-ref="progress" x-show="isDownloading"></progress>
</div>
{% endif %}
@ -92,13 +92,13 @@
document.addEventListener("alpine:init", () => {
Alpine.data("user_pictures", () => ({
is_downloading: false,
isDownloading: false,
loading: true,
pictures: [],
albums: {},
async init() {
this.pictures = await fetch_paginated("{{ url("api:pictures") }}" + "?users_identified={{ object.id }}");
this.pictures = await fetchPaginated("{{ url("api:pictures") }}" + "?users_identified={{ object.id }}");
this.albums = this.pictures.reduce((acc, picture) => {
if (!acc[picture.album]){
acc[picture.album] = [];
@ -109,8 +109,8 @@
this.loading = false;
},
async download_zip(){
this.is_downloading = true;
async downloadZip(){
this.isDownloading = true;
const bar = this.$refs.progress;
bar.value = 0;
bar.max = this.pictures.length;
@ -124,16 +124,16 @@
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("."));
const imgName = p.album + "/IMG_" + p.date.replaceAll(/[:\-]/g, "_") + p.name.slice(p.name.lastIndexOf("."));
return zipWriter.add(
img_name,
imgName,
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;
this.isDownloading = false;
}
}))
});

View File

@ -6,7 +6,7 @@
<link rel="stylesheet" type="text/css" href="{{ statics.css }}" defer>
<script type="text/javascript">
addEventListener("DOMContentLoaded", (event) => {
easymde_factory(
easymdeFactory(
document.getElementById("{{ widget.attrs.id }}"),
"{{ markdown_api_url }}",
);

View File

@ -1,68 +1,77 @@
document.addEventListener('alpine:init', () => {
Alpine.data('counter', () => ({
basket: basket,
document.addEventListener("alpine:init", () => {
Alpine.data("counter", () => ({
// biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
basket: sessionBasket,
errors: [],
sum_basket() {
sumBasket() {
if (!this.basket || Object.keys(this.basket).length === 0) {
return 0;
}
const total = Object.values(this.basket)
.reduce((acc, cur) => acc + cur["qty"] * cur["price"], 0);
const total = Object.values(this.basket).reduce(
(acc, cur) => acc + cur.qty * cur.price,
0,
);
return total / 100;
},
async handle_code(event) {
async handleCode(event) {
const code = $(event.target).find("#code_field").val().toUpperCase();
if (["FIN", "ANN"].includes(code)) {
$(event.target).submit();
} else {
await this.handle_action(event);
await this.handleAction(event);
}
},
async handle_action(event) {
async handleAction(event) {
const payload = $(event.target).serialize();
let request = new Request(click_api_url, {
// biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
const request = new Request(clickApiUrl, {
method: "POST",
body: payload,
headers: {
'Accept': 'application/json',
'X-CSRFToken': csrf_token,
}
})
// biome-ignore lint/style/useNamingConvention: this goes into http headers
Accept: "application/json",
// biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
"X-CSRFToken": csrfToken,
},
});
const response = await fetch(request);
const json = await response.json();
this.basket = json["basket"]
this.errors = json["errors"]
$('form.code_form #code_field').val("").focus();
}
}))
})
this.basket = json.basket;
this.errors = json.errors;
$("form.code_form #code_field").val("").focus();
},
}));
});
$(function () {
$(() => {
/* Autocompletion in the code field */
const code_field = $("#code_field");
const codeField = $("#code_field");
let quantity = "";
code_field.autocomplete({
select: function (event, ui) {
codeField.autocomplete({
select: (event, ui) => {
event.preventDefault();
code_field.val(quantity + ui.item.value);
codeField.val(quantity + ui.item.value);
},
focus: function (event, ui) {
focus: (event, ui) => {
event.preventDefault();
code_field.val(quantity + ui.item.value);
codeField.val(quantity + ui.item.value);
},
source: function (request, response) {
source: (request, response) => {
// biome-ignore lint/performance/useTopLevelRegex: performance impact is minimal
const res = /^(\d+x)?(.*)/i.exec(request.term);
quantity = res[1] || "";
const search = res[2];
const matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i");
response($.grep(products_autocomplete, function(value) {
value = value.tags;
return matcher.test( value );
}));
response(
// biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
$.grep(productsAutocomplete, (value) => {
return matcher.test(value.tags);
}),
);
},
});
@ -73,5 +82,5 @@ $(function () {
});
$("#products").tabs();
code_field.focus();
codeField.focus();
});

View File

@ -59,7 +59,7 @@
{# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #}
<form method="post" action=""
class="code_form" @submit.prevent="handle_code">
class="code_form" @submit.prevent="handleCode">
{% csrf_token %}
<input type="hidden" name="action" value="code">
<label for="code_field"></label>
@ -77,7 +77,7 @@
<template x-for="[id, item] in Object.entries(basket)" :key="id">
<div>
<form method="post" action="" class="inline del_product_form"
@submit.prevent="handle_action">
@submit.prevent="handleAction">
{% csrf_token %}
<input type="hidden" name="action" value="del_product">
<input type="hidden" name="product_id" :value="id">
@ -87,7 +87,7 @@
<span x-text="item['qty'] + item['bonus_qty']"></span>
<form method="post" action="" class="inline add_product_form"
@submit.prevent="handle_action">
@submit.prevent="handleAction">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" :value="id">
@ -104,7 +104,7 @@
</ul>
<p>
<strong>Total: </strong>
<strong x-text="sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 })"></strong>
<strong x-text="sumBasket().toLocaleString(undefined, { minimumFractionDigits: 2 })"></strong>
<strong> €</strong>
</p>
@ -147,7 +147,7 @@
{% for p in categories[category] -%}
<form method="post"
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"
class="form_button add_product_form" @submit.prevent="handle_action">
class="form_button add_product_form" @submit.prevent="handleAction">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" value="{{ p.id }}">
@ -171,9 +171,9 @@
{% block script %}
{{ super() }}
<script>
const csrf_token = "{{ csrf_token }}";
const click_api_url = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}";
const basket = {{ request.session["basket"]|tojson }};
const csrfToken = "{{ csrf_token }}";
const clickApiUrl = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}";
const sessionBasket = {{ request.session["basket"]|tojson }};
const products = {
{%- for p in products -%}
{{ p.id }}: {
@ -183,7 +183,7 @@
},
{%- endfor -%}
};
const products_autocomplete = [
const productsAutocomplete = [
{% for p in products -%}
{
value: "{{ p.code }}",

View File

@ -152,16 +152,24 @@ En ce qui concerne les templates Jinja
et les fichiers SCSS, la norme de formatage
est celle par défaut de `djHTML`.
Pour Javascript, vous pouvez utiliser
Prettier, avec sa configuration par défaut,
qui est plutôt bonne,
mais nous n'avons pas de norme établie pour le projet.
Pour Javascript, nous utilisons [biome](https://biomejs.dev/).
C'est à la fois un formateur et un linter avec très peu de configuration,
un peu comme ruff.
!!!note "Le javascript dans les templates jinja"
Biome n'est pas capable de lire dans les fichiers jinja,
c'est sa principale limitation.
Il est donc recommandé d'éviter de mettre trop de Javascript
directement dans jinja mais de préférer des fichiers dédiés.
### Qualité du code
Pour s'assurer de la qualité du code, Ruff est également utilisé.
Pour s'assurer de la qualité du code, Ruff et Biome sont également utilisés.
Tout comme pour le format, Ruff doit tourner avant chaque commit.
Tout comme pour le format, Ruff et Biome doivent tourner avant chaque commit.
!!!note "to edit or not to edit"
@ -182,6 +190,14 @@ Tout comme pour le format, Ruff doit tourner avant chaque commit.
ruff check --fix
```
Biome se comporte d'une manière très similaire
```bash
npx @biomejs/biome check # Liste toutes les erreurs et leurs catégories
npx @biomejs/biome check --write # Applique tous les fix considérés safe et formate le code
```
## Documentation
La documentation est écrite en markdown, avec les fonctionnalités

View File

@ -354,16 +354,27 @@ Bien installé, il peut effectuer ce travail
à chaque sauvegarde d'un fichier dans son éditeur,
ce qui est très agréable pour travailler.
### Biome
[Site officiel](https://biomejs.dev/)
Puisque Ruff ne fonctionne malheureusement que pour le Python,
nous utilisons Biome pour le javascript.
Biome est également capable d'analyser et formater les fichiers json et css.
Tout comme Ruff, Biome fait office de formateur et de linter.
### DjHTML
[Site officiel](https://github.com/rtts/djhtml)
Ruff permet de formater les fichiers Python,
mais il ne formatte pas les templates et les feuilles de style.
Ruff permet de formater les fichiers Python et Biome les fichiers js,
mais ils ne formattent pas les templates et les feuilles de style.
Pour ça, il faut un autre outil, aisément intégrable
dans la CI : `djHTML`.
En utilisant conjointement Ruff et djHTML,
En utilisant conjointement Ruff, Biome et djHTML,
on arrive donc à la fois à formater les fichiers
Python et les fichiers relatifs au frontend.

View File

@ -99,7 +99,7 @@ votre éditeur pour que Ruff fasse son travail automatiquement à chaque éditio
Nous tenterons de vous faire ici un résumé pour deux éditeurs de textes populaires
que sont VsCode et Sublime Text.
### VsCode
=== "VsCode"
Installez l'extension Ruff pour VsCode.
Ensuite, ajoutez ceci dans votre configuration :
@ -113,9 +113,9 @@ Ensuite, ajoutez ceci dans votre configuration :
}
```
### Sublime Text
=== "Sublime Text"
Vous devez installer ce plugin : https://packagecontrol.io/packages/LSP-ruff.
Vous devez installer le plugin [LSP-ruff](https://packagecontrol.io/packages/LSP-ruff).
Suivez ensuite les instructions données dans la description du plugin.
Dans la configuration de votre projet, ajoutez ceci:
@ -123,7 +123,7 @@ Dans la configuration de votre projet, ajoutez ceci:
```json
{
"settings": {
"lsp_format_on_save": true,
"lsp_format_on_save": true, // Commun à ruff et biome
"LSP": {
"LSP-ruff": {
"enabled": true,
@ -148,3 +148,67 @@ de ruff comme ceci :
]
}
```
## Configurer Biome pour son éditeur
!!!note
Biome est inclus dans les dépendances du projet.
Si vous avez réussi à terminer l'installation, vous n'avez donc pas de configuration
supplémentaire à effectuer.
Pour utiliser Biome, placez-vous à la racine du projet et lancer la commande suivante:
```bash
npx @biomejs/biome check # Pour checker le code avec le linter et le formater
npx @biomejs/biome check --write # Pour appliquer les changemnts
```
Biome va alors faire son travail sur l'ensemble du projet puis vous dire
si des documents ont été reformatés (si vous avez fait `npx @biomejs/biome format --write`)
ou bien s'il y a des erreurs à réparer (si vous avez faire `npx @biomejs/biome lint`) ou les deux (si vous avez fait `npx @biomejs/biome check --write`).
Appeler Biome en ligne de commandes avant de pousser votre code sur Github
est une technique qui marche très bien.
Cependant, vous risquez de souvent l'oublier.
Or, lorsque le code ne respecte pas les Biomes de qualité,
la pipeline bloque les PR sur les branches protégées.
Pour éviter de vous faire régulièrement avoir, vous pouvez configurer
votre éditeur pour que Biome fasse son travail automatiquement à chaque édition d'un fichier.
Nous tenterons de vous faire ici un résumé pour deux éditeurs de textes populaires
que sont VsCode et Sublime Text.
=== "VsCode"
Biome est fourni par le plugin [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome).
Ensuite, ajoutez ceci dans votre configuration :
```json
{
"editor.defaultFormatter": "<other formatter>",
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
```
=== "Sublime Text"
Tout comme pour ruff, il suffit d'installer un plugin lsp [LSP-biome](https://packagecontrol.io/packages/LSP-biome).
Et enfin, dans la configuration de votre projet, ajouter les lignes suivantes :
```json
{
"settings": {
"lsp_format_on_save": true, // Commun à ruff et biome
"LSP": {
"LSP-biome": {
"enabled": true,
}
}
}
}
```

View File

@ -50,7 +50,7 @@
#eboutic .item-list li {
display: flex;
align-items: center;
margin-bottom: 10px
margin-bottom: 10px;
}
#eboutic .item-row {
@ -236,6 +236,10 @@
}
@keyframes bg-in-out {
0% { background-color: white; }
100% { background-color: rgb(216, 236, 255); }
0% {
background-color: white;
}
100% {
background-color: rgb(216, 236, 255);
}
}

View File

@ -14,46 +14,46 @@ const BASKET_ITEMS_COOKIE_NAME = "basket_items";
* @returns {string|null|undefined} the value of the cookie or null if it does not exist, undefined if not found
*/
function getCookie(name) {
// biome-ignore lint/style/useBlockStatements: <explanation>
if (!document.cookie || document.cookie.length === 0) return null;
let found = document.cookie
.split(';')
.map(c => c.trim())
.find(c => c.startsWith(name + '='));
const found = document.cookie
.split(";")
.map((c) => c.trim())
.find((c) => c.startsWith(`${name}=`));
return found === undefined ? undefined : decodeURIComponent(found.split('=')[1]);
return found === undefined ? undefined : decodeURIComponent(found.split("=")[1]);
}
/**
* Fetch the basket items from the associated cookie
* @returns {BasketItem[]|[]} the items in the basket
*/
function get_starting_items() {
function getStartingItems() {
const cookie = getCookie(BASKET_ITEMS_COOKIE_NAME);
if (!cookie) {
return []
return [];
}
// Django cookie backend converts `,` to `\054`
let parsed = JSON.parse(cookie.replace(/\\054/g, ','));
let parsed = JSON.parse(cookie.replace(/\\054/g, ","));
if (typeof parsed === "string") {
// In some conditions, a second parsing is needed
parsed = JSON.parse(parsed);
}
const res = Array.isArray(parsed) ? parsed : [];
return res.filter((i) => !!document.getElementById(i.id))
return res.filter((i) => !!document.getElementById(i.id));
}
document.addEventListener('alpine:init', () => {
Alpine.data('basket', () => ({
items: get_starting_items(),
document.addEventListener("alpine:init", () => {
Alpine.data("basket", () => ({
items: getStartingItems(),
/**
* Get the total price of the basket
* @returns {number} The total price of the basket
*/
get_total() {
return this.items
.reduce((acc, item) => acc + item["quantity"] * item["unit_price"], 0);
getTotal() {
return this.items.reduce((acc, item) => acc + item.quantity * item.unit_price, 0);
},
/**
@ -62,38 +62,40 @@ document.addEventListener('alpine:init', () => {
*/
add(item) {
item.quantity++;
this.set_cookies();
this.setCookies();
},
/**
* Remove 1 to the quantity of an item in the basket
* @param {BasketItem} item_id
*/
remove(item_id) {
const index = this.items.findIndex(e => e.id === item_id);
remove(itemId) {
const index = this.items.findIndex((e) => e.id === itemId);
if (index < 0) return;
if (index < 0) {
return;
}
this.items[index].quantity -= 1;
if (this.items[index].quantity === 0) {
this.items = this.items.filter((e) => e.id !== this.items[index].id);
}
this.set_cookies();
this.setCookies();
},
/**
* Remove all the items from the basket & cleans the catalog CSS classes
*/
clear_basket() {
clearBasket() {
this.items = [];
this.set_cookies();
this.setCookies();
},
/**
* Set the cookie in the browser with the basket items
* ! the cookie survives an hour
*/
set_cookies() {
setCookies() {
if (this.items.length === 0) {
document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=;Max-Age=0`;
} else {
@ -108,18 +110,19 @@ document.addEventListener('alpine:init', () => {
* @param {number} price The unit price of the product
* @returns {BasketItem} The created item
*/
create_item(id, name, price) {
let new_item = {
id: id,
name: name,
createItem(id, name, price) {
const newItem = {
id,
name,
quantity: 0,
unit_price: price
// biome-ignore lint/style/useNamingConvention: used by django backend
unit_price: price,
};
this.items.push(new_item);
this.add(new_item);
this.items.push(newItem);
this.add(newItem);
return new_item;
return newItem;
},
/**
@ -129,16 +132,16 @@ document.addEventListener('alpine:init', () => {
* @param {string} name The name of the product
* @param {number} price The unit price of the product
*/
add_from_catalog(id, name, price) {
let item = this.items.find(e => e.id === id)
addFromCatalog(id, name, price) {
let item = this.items.find((e) => e.id === id);
// if the item is not in the basket, we create it
// else we add + 1 to it
if (!item) {
item = this.create_item(id, name, price);
} else {
if (item) {
this.add(item);
} else {
item = this.createItem(id, name, price);
}
},
}))
})
}));
});

View File

@ -3,18 +3,23 @@
* @enum {number}
*/
const BillingInfoReqState = {
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
SUCCESS: 1,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
FAILURE: 2,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
SENDING: 3,
};
document.addEventListener("alpine:init", () => {
Alpine.store("billing_inputs", {
data: et_data,
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
data: etData,
async fill() {
document.getElementById("bank-submit-button").disabled = true;
const res = await fetch(et_data_url);
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
const res = await fetch(etDataUrl);
if (res.ok) {
this.data = await res.json();
document.getElementById("bank-submit-button").disabled = false;
@ -24,54 +29,57 @@ document.addEventListener("alpine:init", () => {
Alpine.data("billing_infos", () => ({
/** @type {BillingInfoReqState | null} */
req_state: null,
reqState: null,
async send_form() {
this.req_state = BillingInfoReqState.SENDING;
async sendForm() {
this.reqState = BillingInfoReqState.SENDING;
const form = document.getElementById("billing_info_form");
document.getElementById("bank-submit-button").disabled = true;
let payload = Object.fromEntries(
const payload = Object.fromEntries(
Array.from(form.querySelectorAll("input, select"))
.filter((elem) => elem.type !== "submit" && elem.value)
.map((elem) => [elem.name, elem.value]),
);
const res = await fetch(billing_info_url, {
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
const res = await fetch(billingInfoUrl, {
method: "PUT",
body: JSON.stringify(payload),
});
this.req_state = res.ok
this.reqState = res.ok
? BillingInfoReqState.SUCCESS
: BillingInfoReqState.FAILURE;
if (res.status === 422) {
const errors = (await res.json())["detail"].map((err) => err["loc"]).flat();
Array.from(form.querySelectorAll("input"))
.filter((elem) => errors.includes(elem.name))
.forEach((elem) => {
const errors = (await res.json()).detail.flatMap((err) => err.loc);
for (const elem of Array.from(form.querySelectorAll("input")).filter((elem) =>
errors.includes(elem.name),
)) {
elem.setCustomValidity(gettext("Incorrect value"));
elem.reportValidity();
elem.oninput = () => elem.setCustomValidity("");
});
}
} else if (res.ok) {
Alpine.store("billing_inputs").fill();
}
},
get_alert_color() {
if (this.req_state === BillingInfoReqState.SUCCESS) {
getAlertColor() {
if (this.reqState === BillingInfoReqState.SUCCESS) {
return "green";
}
if (this.req_state === BillingInfoReqState.FAILURE) {
if (this.reqState === BillingInfoReqState.FAILURE) {
return "red";
}
return "";
},
get_alert_message() {
if (this.req_state === BillingInfoReqState.SUCCESS) {
return billing_info_success_message;
getAlertMessage() {
if (this.reqState === BillingInfoReqState.SUCCESS) {
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
return billingInfoSuccessMessage;
}
if (this.req_state === BillingInfoReqState.FAILURE) {
return billing_info_failure_message;
if (this.reqState === BillingInfoReqState.FAILURE) {
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
return billingInfoFailureMessage;
}
return "";
},

View File

@ -56,11 +56,11 @@
{# Total price #}
<li style="margin-top: 20px">
<span class="item-name"><strong>{% trans %}Basket amount: {% endtrans %}</strong></span>
<span x-text="get_total().toFixed(2) + ' €'" class="item-price"></span>
<span x-text="getTotal().toFixed(2) + ' €'" class="item-price"></span>
</li>
</ul>
<div class="catalog-buttons">
<button @click="clear_basket()" class="btn btn-grey">
<button @click="clearBasket()" class="btn btn-grey">
<i class="fa fa-trash"></i>
{% trans %}Clear{% endtrans %}
</button>
@ -106,7 +106,7 @@
id="{{ p.id }}"
class="product-button"
:class="{selected: items.some((i) => i.id === {{ p.id }})}"
@click='add_from_catalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})'
@click='addFromCatalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})'
>
{% if p.icon %}
<img class="product-image" src="{{ p.icon.url }}"

View File

@ -56,7 +56,7 @@
<div
class="collapse"
:class="{'shadow': collapsed}"
x-data="{collapsed: !billing_info_exist}"
x-data="{collapsed: !billingInfoExist}"
x-cloak
>
<div class="collapse-header clickable" @click="collapsed = !collapsed">
@ -73,27 +73,27 @@
x-data="billing_infos"
x-show="collapsed"
x-transition.scale.origin.top
@submit.prevent="await send_form()"
@submit.prevent="await sendForm()"
>
{% csrf_token %}
{{ billing_form }}
<br>
<br>
<div
x-show="[BillingInfoReqState.SUCCESS, BillingInfoReqState.FAILURE].includes(req_state)"
x-show="[BillingInfoReqState.SUCCESS, BillingInfoReqState.FAILURE].includes(reqState)"
class="alert"
:class="'alert-' + get_alert_color()"
:class="'alert-' + getAlertColor()"
x-transition
>
<div class="alert-main" x-text="get_alert_message()"></div>
<div class="clickable" @click="req_state = null">
<div class="alert-main" x-text="getAlertMessage()"></div>
<div class="clickable" @click="reqState = null">
<i class="fa fa-close"></i>
</div>
</div>
<input
type="submit" class="btn btn-blue clickable"
value="{% trans %}Validate{% endtrans %}"
:disabled="req_state === BillingInfoReqState.SENDING"
:disabled="reqState === BillingInfoReqState.SENDING"
>
</form>
</div>
@ -141,16 +141,16 @@
{% block script %}
<script>
const billing_info_url = '{{ url("api:put_billing_info", user_id=request.user.id) }}';
const et_data_url = '{{ url("api:etransaction_data") }}';
const billing_info_exist = {{ "true" if billing_infos else "false" }};
const billing_info_success_message = "{% trans %}Billing info registration success{% endtrans %}";
const billing_info_failure_message = "{% trans %}Billing info registration failure{% endtrans %}";
const billingInfoUrl = '{{ url("api:put_billing_info", user_id=request.user.id) }}';
const etDataUrl = '{{ url("api:etransaction_data") }}';
const billingInfoExist = {{ "true" if billing_infos else "false" }};
const billingInfoSuccessMessage = "{% trans %}Billing info registration success{% endtrans %}";
const billingInfoFailureMessage = "{% trans %}Billing info registration failure{% endtrans %}";
{% if billing_infos %}
const et_data = {{ billing_infos|safe }}
const etData = {{ billing_infos|safe }}
{% else %}
const et_data = {}
const etData = {}
{% endif %}
</script>
{{ super() }}

File diff suppressed because one or more lines are too long

156
package-lock.json generated
View File

@ -20,6 +20,7 @@
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@biomejs/biome": "1.9.3",
"babel-loader": "^9.2.1",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
@ -1817,6 +1818,161 @@
"node": ">=6.9.0"
}
},
"node_modules/@biomejs/biome": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.3.tgz",
"integrity": "sha512-POjAPz0APAmX33WOQFGQrwLvlu7WLV4CFJMlB12b6ZSg+2q6fYu9kZwLCOA+x83zXfcPd1RpuWOKJW0GbBwLIQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.21.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "1.9.3",
"@biomejs/cli-darwin-x64": "1.9.3",
"@biomejs/cli-linux-arm64": "1.9.3",
"@biomejs/cli-linux-arm64-musl": "1.9.3",
"@biomejs/cli-linux-x64": "1.9.3",
"@biomejs/cli-linux-x64-musl": "1.9.3",
"@biomejs/cli-win32-arm64": "1.9.3",
"@biomejs/cli-win32-x64": "1.9.3"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.3.tgz",
"integrity": "sha512-QZzD2XrjJDUyIZK+aR2i5DDxCJfdwiYbUKu9GzkCUJpL78uSelAHAPy7m0GuPMVtF/Uo+OKv97W3P9nuWZangQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.3.tgz",
"integrity": "sha512-vSCoIBJE0BN3SWDFuAY/tRavpUtNoqiceJ5PrU3xDfsLcm/U6N93JSM0M9OAiC/X7mPPfejtr6Yc9vSgWlEgVw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.3.tgz",
"integrity": "sha512-vJkAimD2+sVviNTbaWOGqEBy31cW0ZB52KtpVIbkuma7PlfII3tsLhFa+cwbRAcRBkobBBhqZ06hXoZAN8NODQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.3.tgz",
"integrity": "sha512-VBzyhaqqqwP3bAkkBrhVq50i3Uj9+RWuj+pYmXrMDgjS5+SKYGE56BwNw4l8hR3SmYbLSbEo15GcV043CDSk+Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.3.tgz",
"integrity": "sha512-x220V4c+romd26Mu1ptU+EudMXVS4xmzKxPVb9mgnfYlN4Yx9vD5NZraSx/onJnd3Gh/y8iPUdU5CDZJKg9COA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.3.tgz",
"integrity": "sha512-TJmnOG2+NOGM72mlczEsNki9UT+XAsMFAOo8J0me/N47EJ/vkLXxf481evfHLlxMejTY6IN8SdRSiPVLv6AHlA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.3.tgz",
"integrity": "sha512-lg/yZis2HdQGsycUvHWSzo9kOvnGgvtrYRgoCEwPBwwAL8/6crOp3+f47tPwI/LI1dZrhSji7PNsGKGHbwyAhw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.3.tgz",
"integrity": "sha512-cQMy2zanBkVLpmmxXdK6YePzmZx0s5Z7KEnwmrW54rcXK3myCNbQa09SwGZ8i/8sLw0H9F3X7K4rxVNGU8/D4Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",

View File

@ -6,26 +6,26 @@
"scripts": {
"compile": "webpack --mode production",
"compile-dev": "webpack --mode development",
"serve": "webpack --mode development --watch"
"serve": "webpack --mode development --watch",
"check": "biome check --write"
},
"keywords": [],
"author": "",
"license": "GPL-3.0-only",
"sideEffects": [
".css"
],
"sideEffects": [".css"],
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@biomejs/biome": "1.9.3",
"babel-loader": "^9.2.1",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
"expose-loader": "^5.0.0",
"mini-css-extract-plugin": "^2.9.1",
"source-map-loader": "^5.0.0",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0"
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.6.0",

View File

@ -125,14 +125,14 @@
then fetch the corresponding data from the API.
This data will then be displayed on the result part of the page.
#}
const page_default = 1;
const page_size_default = 100;
const pageDefault = 1;
const pageSizeDefault = 100;
document.addEventListener("alpine:init", () => {
Alpine.data("uv_search", () => ({
uvs: [],
loading: false,
page: page_default,
page_size: page_size_default,
page: pageDefault,
pageSize: pageSizeDefault,
search: "",
department: [],
credit_type: [],
@ -142,12 +142,12 @@
update: undefined,
async initialize_args() {
async initializeArgs() {
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.page = parseInt(url.get("page")) || pageDefault;;
this.pageSize = parseInt(url.get("pageSize")) || pageSizeDefault;
this.search = url.get("search") || "";
this.department = url.getAll("department");
this.credit_type = url.getAll("credit_type");
@ -164,18 +164,18 @@
this.update = Alpine.debounce(async () => {
{# Create the whole url before changing everything all at once #}
let first = this.to_change.shift();
let url = update_query_string(first.param, first.value, History.NONE);
let url = updateQueryString(first.param, first.value, History.NONE);
this.to_change.forEach((value) => {
url = update_query_string(value.param, value.value, History.NONE, url);
url = updateQueryString(value.param, value.value, History.NONE, url);
})
update_query_string(first.param, first.value, this.pushstate, url);
await this.fetch_data(); {# reload data on form change #}
updateQueryString(first.param, first.value, this.pushstate, url);
await this.fetchData(); {# reload data on form change #}
this.to_change = [];
this.pushstate = History.PUSH;
}, 50);
let search_params = ["search", "department", "credit_type", "semester"];
let pagination_params = ["page", "page_size"];
let pagination_params = ["page", "pageSize"];
search_params.forEach((param) => {
this.$watch(param, async (value) => {
@ -184,8 +184,8 @@
return;
}
{# Reset pagination on search #}
this.page = page_default;
this.page_size = page_size_default;
this.page = pageDefault;
this.pageSize = pageSizeDefault;
});
});
search_params.concat(pagination_params).forEach((param) => {
@ -195,13 +195,13 @@
});
});
window.addEventListener("popstate", async (event) => {
await this.initialize_args();
await this.initializeArgs();
});
await this.initialize_args();
await this.initializeArgs();
},
async fetch_data() {
async fetchData() {
this.loading = true;
const url = "{{ url("api:fetch_uvs") }}" + window.location.search;
this.uvs = await (await fetch(url)).json();
@ -209,7 +209,7 @@
},
max_page() {
return Math.ceil(this.uvs.count / this.page_size);
return Math.ceil(this.uvs.count / this.pageSize);
}
}))
})

View File

@ -10,8 +10,8 @@
*/
class PictureWithIdentifications {
identifications = null;
image_loading = false;
identifications_loading = false;
imageLoading = false;
identificationsLoading = false;
/**
* @param {Picture} picture
@ -19,10 +19,11 @@ class PictureWithIdentifications {
constructor(picture) {
Object.assign(this, picture);
}
/**
* @param {Picture} picture
*/
static from_picture(picture) {
static fromPicture(picture) {
return new PictureWithIdentifications(picture);
}
@ -32,19 +33,19 @@ class PictureWithIdentifications {
* @param {?Object=} options
* @return {Promise<void>}
*/
async load_identifications(options) {
if (this.identifications_loading) {
async loadIdentifications(options) {
if (this.identificationsLoading) {
return; // The users are already being fetched.
}
if (!!this.identifications && !options?.force_reload) {
if (!!this.identifications && !options?.forceReload) {
// The users are already fetched
// and the user does not want to force the reload
return;
}
this.identifications_loading = true;
this.identificationsLoading = true;
const url = `/api/sas/picture/${this.id}/identified`;
this.identifications = await (await fetch(url)).json();
this.identifications_loading = false;
this.identificationsLoading = false;
}
/**
@ -55,12 +56,12 @@ class PictureWithIdentifications {
const img = new Image();
img.src = this.compressed_url;
if (!img.complete) {
this.image_loading = true;
this.imageLoading = true;
img.addEventListener("load", () => {
this.image_loading = false;
this.imageLoading = false;
});
}
await this.load_identifications();
await this.loadIdentifications();
}
}
@ -77,13 +78,18 @@ document.addEventListener("alpine:init", () => {
* when loading the page at the beginning
* @type PictureWithIdentifications
**/
current_picture: {
currentPicture: {
// biome-ignore lint/style/useNamingConvention: json is snake_case
is_moderated: true,
id: null,
name: "",
// biome-ignore lint/style/useNamingConvention: json is snake_case
display_name: "",
// biome-ignore lint/style/useNamingConvention: json is snake_case
compressed_url: "",
// biome-ignore lint/style/useNamingConvention: json is snake_case
profile_url: "",
// biome-ignore lint/style/useNamingConvention: json is snake_case
full_size_url: "",
owner: "",
date: new Date(),
@ -93,12 +99,12 @@ document.addEventListener("alpine:init", () => {
* The picture which will be displayed next if the user press the "next" button
* @type ?PictureWithIdentifications
**/
next_picture: null,
nextPicture: null,
/**
* The picture which will be displayed next if the user press the "previous" button
* @type ?PictureWithIdentifications
**/
previous_picture: null,
previousPicture: null,
/**
* The select2 component used to identify users
**/
@ -110,7 +116,7 @@ document.addEventListener("alpine:init", () => {
* Error message when a moderation operation fails
* @type string
**/
moderation_error: "",
moderationError: "",
/**
* Method of pushing new url to the browser history
* Used by popstate event and always reset to it's default value when used
@ -119,121 +125,119 @@ document.addEventListener("alpine:init", () => {
pushstate: History.PUSH,
async init() {
this.pictures = (await fetch_paginated(picture_endpoint)).map(
PictureWithIdentifications.from_picture,
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
this.pictures = (await fetchPaginated(pictureEndpoint)).map(
PictureWithIdentifications.fromPicture,
);
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
this.selector = sithSelect2({
element: $(this.$refs.search),
data_source: remote_data_source("/api/user/search", {
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
dataSource: remoteDataSource("/api/user/search", {
excluded: () => [
...(this.current_picture.identifications || []).map(
(i) => i.user.id,
),
...(this.currentPicture.identifications || []).map((i) => i.user.id),
],
result_converter: (obj) => Object({ ...obj, text: obj.display_name }),
resultConverter: (obj) => new Object({ ...obj, text: obj.display_name }),
}),
picture_getter: (user) => user.profile_pict,
pictureGetter: (user) => user.profile_pict,
});
this.current_picture = this.pictures.find(
(i) => i.id === first_picture_id,
);
this.$watch("current_picture", (current, previous) => {
if (current === previous){ /* Avoid recursive updates */
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from picture.jinja
this.currentPicture = this.pictures.find((i) => i.id === firstPictureId);
this.$watch("currentPicture", (current, previous) => {
if (current === previous) {
/* Avoid recursive updates */
return;
}
this.update_picture();
this.updatePicture();
});
window.addEventListener("popstate", async (event) => {
if (!event.state || event.state.sas_picture_id === undefined) {
if (!event.state || event.state.sasPictureId === undefined) {
return;
}
this.pushstate = History.REPLACE;
this.current_picture = this.pictures.find(
(i) => i.id === parseInt(event.state.sas_picture_id),
this.currentPicture = this.pictures.find(
(i) => i.id === Number.parseInt(event.state.sasPictureId),
);
});
this.pushstate = History.REPLACE; /* Avoid first url push */
await this.update_picture();
await this.updatePicture();
},
/**
* Update the page.
* Called when the `current_picture` property changes.
* Called when the `currentPicture` property changes.
*
* The url is modified without reloading the page,
* and the previous picture, the next picture and
* the list of identified users are updated.
*/
async update_picture() {
const update_args = [
{ sas_picture_id: this.current_picture.id },
async updatePicture() {
const updateArgs = [
{ sasPictureId: this.currentPicture.id },
"",
`/sas/picture/${this.current_picture.id}/`,
`/sas/picture/${this.currentPicture.id}/`,
];
if (this.pushstate === History.REPLACE) {
window.history.replaceState(...update_args);
window.history.replaceState(...updateArgs);
this.pushstate = History.PUSH;
} else {
window.history.pushState(...update_args);
window.history.pushState(...updateArgs);
}
this.moderation_error = "";
const index = this.pictures.indexOf(this.current_picture);
this.previous_picture = this.pictures[index - 1] || null;
this.next_picture = this.pictures[index + 1] || null;
await this.current_picture.load_identifications();
this.$refs.main_picture?.addEventListener("load", () => {
this.moderationError = "";
const index = this.pictures.indexOf(this.currentPicture);
this.previousPicture = this.pictures[index - 1] || null;
this.nextPicture = this.pictures[index + 1] || null;
await this.currentPicture.loadIdentifications();
this.$refs.mainPicture?.addEventListener("load", () => {
// once the current picture is loaded,
// start preloading the next and previous pictures
this.next_picture?.preload();
this.previous_picture?.preload();
this.nextPicture?.preload();
this.previousPicture?.preload();
});
},
async moderate_picture() {
const res = await fetch(
`/api/sas/picture/${this.current_picture.id}/moderate`,
{
async moderatePicture() {
const res = await fetch(`/api/sas/picture/${this.currentPicture.id}/moderate`, {
method: "PATCH",
},
);
});
if (!res.ok) {
this.moderation_error = `${gettext("Couldn't moderate picture")} : ${res.statusText}`;
this.moderationError = `${gettext("Couldn't moderate picture")} : ${res.statusText}`;
return;
}
this.current_picture.is_moderated = true;
this.current_picture.asked_for_removal = false;
this.currentPicture.is_moderated = true;
this.currentPicture.askedForRemoval = false;
},
async delete_picture() {
const res = await fetch(`/api/sas/picture/${this.current_picture.id}`, {
async deletePicture() {
const res = await fetch(`/api/sas/picture/${this.currentPicture.id}`, {
method: "DELETE",
});
if (!res.ok) {
this.moderation_error =
gettext("Couldn't delete picture") + " : " + res.statusText;
this.moderationError = `${gettext("Couldn't delete picture")} : ${res.statusText}`;
return;
}
this.pictures.splice(this.pictures.indexOf(this.current_picture), 1);
this.pictures.splice(this.pictures.indexOf(this.currentPicture), 1);
if (this.pictures.length === 0) {
// The deleted picture was the only one in the list.
// As the album is now empty, go back to the parent page
document.location.href = album_url;
// biome-ignore lint/correctness/noUndeclaredVariables: imported from picture.jinja
document.location.href = albumUrl;
}
this.current_picture = this.next_picture || this.previous_picture;
this.currentPicture = this.nextPicture || this.previousPicture;
},
/**
* Send the identification request and update the list of identified users.
*/
async submit_identification() {
const url = `/api/sas/picture/${this.current_picture.id}/identified`;
async submitIdentification() {
const url = `/api/sas/picture/${this.currentPicture.id}/identified`;
await fetch(url, {
method: "PUT",
body: JSON.stringify(this.selector.val().map((i) => parseInt(i))),
body: JSON.stringify(this.selector.val().map((i) => Number.parseInt(i))),
});
// refresh the identified users list
await this.current_picture.load_identifications({ force_reload: true });
await this.currentPicture.loadIdentifications({ forceReload: true });
this.selector.empty().trigger("change");
},
@ -242,23 +246,22 @@ document.addEventListener("alpine:init", () => {
* @param {PictureIdentification} identification
* @return {boolean}
*/
can_be_removed(identification) {
return user_is_sas_admin || identification.user.id === user_id;
canBeRemoved(identification) {
// biome-ignore lint/correctness/noUndeclaredVariables: imported from picture.jinja
return userIsSasAdmin || identification.user.id === userId;
},
/**
* Untag a user from the current picture
* @param {PictureIdentification} identification
*/
async remove_identification(identification) {
async removeIdentification(identification) {
const res = await fetch(`/api/sas/relation/${identification.id}`, {
method: "DELETE",
});
if (res.ok && Array.isArray(this.current_picture.identifications)) {
this.current_picture.identifications =
this.current_picture.identifications.filter(
(i) => i.id !== identification.id,
);
if (res.ok && Array.isArray(this.currentPicture.identifications)) {
this.currentPicture.identifications =
this.currentPicture.identifications.filter((i) => i.id !== identification.id);
}
},
}));

View File

@ -83,7 +83,7 @@
</a>
</template>
</div>
{{ paginate_alpine("page", "nb_pages()") }}
{{ paginate_alpine("page", "nbPages()") }}
</div>
{% if is_sas_admin %}
@ -116,14 +116,14 @@
loading: false,
async init() {
await this.fetch_pictures();
await this.fetchPictures();
this.$watch("page", () => {
update_query_string("page",
updateQueryString("page",
this.page === 1 ? null : this.page,
this.pushstate
);
this.pushstate = History.PUSH;
this.fetch_pictures();
this.fetchPictures();
});
window.addEventListener("popstate", () => {
@ -134,7 +134,7 @@
});
},
async fetch_pictures() {
async fetchPictures() {
this.loading=true;
const url = "{{ url("api:pictures") }}"
+"?album_id={{ album.id }}"
@ -144,7 +144,7 @@
this.loading = false;
},
nb_pages() {
nbPages() {
return Math.ceil(this.pictures.count / {{ settings.SITH_SAS_IMAGES_PER_PAGE }});
}
}))

View File

@ -17,21 +17,21 @@
{% block content %}
<main x-data="picture_viewer">
<code>
<a href="{{ url('sas:main') }}">SAS</a> / {{ print_path(album) }} <span x-text="current_picture.name"></span>
<a href="{{ url('sas:main') }}">SAS</a> / {{ print_path(album) }} <span x-text="currentPicture.name"></span>
</code>
<br>
<div class="title">
<h3 x-text="current_picture.name"></h3>
<h4 x-text="`${pictures.indexOf(current_picture) + 1 } / ${pictures.length}`"></h4>
<h3 x-text="currentPicture.name"></h3>
<h4 x-text="`${pictures.indexOf(currentPicture) + 1 } / ${pictures.length}`"></h4>
</div>
<br>
<template x-if="!current_picture.is_moderated">
<template x-if="!currentPicture.is_moderated">
<div class="alert alert-red">
<div class="alert-main">
<template x-if="current_picture.asked_for_removal">
<template x-if="currentPicture.askedForRemoval">
<span class="important">{% trans %}Asked for removal{% endtrans %}</span>
</template>
<p>
@ -43,14 +43,14 @@
</div>
<div>
<div>
<button class="btn btn-blue" @click="moderate_picture()">
<button class="btn btn-blue" @click="moderatePicture()">
{% trans %}Moderate{% endtrans %}
</button>
<button class="btn btn-red" @click.prevent="delete_picture()">
<button class="btn btn-red" @click.prevent="deletePicture()">
{% trans %}Delete{% endtrans %}
</button>
</div>
<p x-show="!!moderation_error" x-text="moderation_error"></p>
<p x-show="!!moderationError" x-text="moderationError"></p>
</div>
</div>
</template>
@ -58,12 +58,12 @@
<div class="container" id="pict">
<div class="main">
<div class="photo" :aria-busy="current_picture.image_loading">
<div class="photo" :aria-busy="currentPicture.imageLoading">
<img
:src="current_picture.compressed_url"
:alt="current_picture.name"
:src="currentPicture.compressed_url"
:alt="currentPicture.name"
id="main-picture"
x-ref="main_picture"
x-ref="mainPicture"
/>
</div>
@ -76,13 +76,13 @@
<span
x-text="Intl.DateTimeFormat(
'{{ LANGUAGE_CODE }}', {dateStyle: 'long'}
).format(new Date(current_picture.date))"
).format(new Date(currentPicture.date))"
>
</span>
</div>
<div>
<span>{% trans %}Owner: {% endtrans %}</span>
<a :href="current_picture.owner.profile_url" x-text="current_picture.owner.display_name"></a>
<a :href="currentPicture.owner.profile_url" x-text="currentPicture.owner.display_name"></a>
</div>
</div>
</div>
@ -91,14 +91,14 @@
<h5>{% trans %}Tools{% endtrans %}</h5>
<div>
<div>
<a class="text" :href="current_picture.full_size_url">
<a class="text" :href="currentPicture.full_size_url">
{% trans %}HD version{% endtrans %}
</a>
<br>
<a class="text danger" href="?ask_removal">{% trans %}Ask for removal{% endtrans %}</a>
</div>
<div class="buttons">
<a class="button" :href="`/sas/picture/${current_picture.id}/edit/`"><i class="fa-regular fa-pen-to-square edit-action"></i></a>
<a class="button" :href="`/sas/picture/${currentPicture.id}/edit/`"><i class="fa-regular fa-pen-to-square edit-action"></i></a>
<a class="button" href="?rotate_left"><i class="fa-solid fa-rotate-left"></i></a>
<a class="button" href="?rotate_right"><i class="fa-solid fa-rotate-right"></i></a>
</div>
@ -110,23 +110,23 @@
<div class="subsection">
<div class="navigation">
<div id="prev" class="clickable">
<template x-if="previous_picture">
<template x-if="previousPicture">
<div
@keyup.left.window="current_picture = previous_picture"
@click="current_picture = previous_picture"
@keyup.left.window="currentPicture = previousPicture"
@click="currentPicture = previousPicture"
>
<img :src="previous_picture.thumb_url" alt="{% trans %}Previous picture{% endtrans %}"/>
<img :src="previousPicture.thumb_url" alt="{% trans %}Previous picture{% endtrans %}"/>
<div class="overlay">←</div>
</div>
</template>
</div>
<div id="next" class="clickable">
<template x-if="next_picture">
<template x-if="nextPicture">
<div
@keyup.right.window="current_picture = next_picture"
@click="current_picture = next_picture"
@keyup.right.window="currentPicture = nextPicture"
@click="currentPicture = nextPicture"
>
<img :src="next_picture.thumb_url" alt="{% trans %}Previous picture{% endtrans %}"/>
<img :src="nextPicture.thumb_url" alt="{% trans %}Previous picture{% endtrans %}"/>
<div class="overlay">→</div>
</div>
</template>
@ -136,14 +136,14 @@
<div class="tags">
<h5>{% trans %}People{% endtrans %}</h5>
{% if user.was_subscribed %}
<form @submit.prevent="submit_identification" x-show="!!selector">
<form @submit.prevent="submitIdentification" x-show="!!selector">
<select x-ref="search" multiple="multiple"></select>
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
{% endif %}
<ul>
<template
x-for="identification in (current_picture.identifications || [])"
x-for="identification in (currentPicture.identifications || [])"
:key="identification.id"
>
<li>
@ -151,12 +151,12 @@
<img class="profile-pic" :src="identification.user.profile_pict" alt="image de profil"/>
<span x-text="identification.user.display_name"></span>
</a>
<template x-if="can_be_removed(identification)">
<a class="delete clickable" @click="remove_identification(identification)"><i class="fa fa-times fa-xl delete-action"></i></a>
<template x-if="canBeRemoved(identification)">
<a class="delete clickable" @click="removeIdentification(identification)"><i class="fa fa-times fa-xl delete-action"></i></a>
</template>
</li>
</template>
<template x-if="current_picture.identifications_loading">
<template x-if="currentPicture.identificationsLoading">
{# shadow element that exists only to put the loading wheel below
the list of identified people #}
<li class="loader" aria-busy="true"></li>
@ -171,10 +171,10 @@
{% block script %}
{{ super() }}
<script>
const picture_endpoint = "{{ url("api:pictures") + "?album_id=" + album.id|string }}";
const album_url = "{{ album.get_absolute_url() }}";
const first_picture_id = {{ picture.id }}; {# id of the first picture to show after page load #}
const user_id = {{ user.id }};
const user_is_sas_admin = {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }}
const pictureEndpoint = "{{ url("api:pictures") + "?album_id=" + album.id|string }}";
const albumUrl = "{{ album.get_absolute_url() }}";
const firstPictureId = {{ picture.id }}; {# id of the first picture to show after page load #}
const userId = {{ user.id }};
const userIsSasAdmin = {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }}
</script>
{% endblock %}

View File

@ -1,23 +1,21 @@
const glob = require('glob');
const path = require('path');
const webpack = require("webpack");
const glob = require("glob");
// biome-ignore lint/correctness/noNodejsModules: this is backend side
const path = require("node:path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
entry: glob.sync('./!(static)/static/webpack/**?(-)index.js').reduce((obj, el) => {
obj[path.parse(el).name] = './' + el;
entry: glob.sync("./!(static)/static/webpack/**?(-)index.js").reduce((obj, el) => {
obj[path.parse(el).name] = `./${el}`;
return obj;
}, {}),
output: {
filename: '[name].js',
path: path.resolve(__dirname, './staticfiles/generated/webpack'),
clean: true
filename: "[name].js",
path: path.resolve(__dirname, "./staticfiles/generated/webpack"),
clean: true,
},
plugins: [
new MiniCssExtractPlugin(),
],
plugins: [new MiniCssExtractPlugin()],
optimization: {
minimizer: [
"...",
@ -29,9 +27,10 @@ module.exports = {
terserOptions: {
mangle: true,
compress: {
// biome-ignore lint/style/useNamingConvention: this is how the underlying library wants it
drop_console: true,
},
}
},
}),
],
},
@ -40,14 +39,11 @@ module.exports = {
{
test: /\.css$/,
sideEffects: true,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
],
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(jpe?g|png|gif|woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource'
test: /\.(jpe?g|png|gif)$/i,
type: "asset/resource",
},
{
test: /\.m?js$/,
@ -55,9 +51,9 @@ module.exports = {
use: {
loader: "babel-loader",
options: {
presets: ['@babel/preset-env']
}
}
presets: ["@babel/preset-env"],
},
},
},
{
test: /\.js$/,
@ -70,17 +66,17 @@ module.exports = {
options: {
exposes: [
{
globalName: ['$'],
override: true
globalName: ["$"],
override: true,
},
{
globalName: ['jQuery'],
override: true
globalName: ["jQuery"],
override: true,
},
{
globalName: ['window.jQuery'],
override: true
}
globalName: ["window.jQuery"],
override: true,
},
],
},
},