Merge pull request #870 from ae-utbm/taiste

Counter state improvement, Stock app removal, lot of work on Webpack and more
This commit is contained in:
thomas girod 2024-10-11 15:18:12 +02:00 committed by GitHub
commit 6a31f38ceb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
217 changed files with 14592 additions and 14574 deletions

View File

@ -1,4 +1,4 @@
name: Sith 3 CI name: Sith CI
on: on:
push: push:

View File

@ -31,7 +31,7 @@ jobs:
script_stop: true script_stop: true
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action # See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
script: | script: |
export PATH="/home/sith/.local/bin:$PATH" export PATH="/home/sith/.local/bin:$PATH"
pushd ${{secrets.SITH_PATH}} pushd ${{secrets.SITH_PATH}}

View File

@ -1,4 +1,4 @@
name: Sith3 taiste name: Sith taiste
on: on:
push: push:
@ -30,7 +30,7 @@ jobs:
script_stop: true script_stop: true
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action # See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
script: | script: |
export PATH="$HOME/.poetry/bin:$PATH" export PATH="$HOME/.poetry/bin:$PATH"
pushd ${{secrets.SITH_PATH}} pushd ${{secrets.SITH_PATH}}

View File

@ -1,13 +1,18 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.5.7 rev: v0.6.9
hooks: hooks:
- id: ruff # just check the code, and print the errors - id: ruff # just check the code, and print the errors
- id: ruff # actually fix the fixable errors, but print nothing - id: ruff # actually fix the fixable errors, but print nothing
args: ["--fix", "--silent"] args: ["--fix", "--silent"]
# Run the formatter. # Run the formatter.
- id: ruff-format - 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 - repo: https://github.com/rtts/djhtml
rev: 3.0.6 rev: 3.0.6
hooks: hooks:

View File

@ -2,14 +2,15 @@
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](#) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](#)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![CI status](https://github.com/ae-utbm/sith3/actions/workflows/ci.yml/badge.svg)](#) [![CI status](https://github.com/ae-utbm/sith/actions/workflows/ci.yml/badge.svg)](#)
[![Docs status](https://github.com/ae-utbm/sith3/actions/workflows/deploy_docs.yml/badge.svg)](https://ae-utbm.github.io/sith3) [![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/) [![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) [![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/). ### This is the source code of the UTBM's student association available at [https://ae.utbm.fr/](https://ae.utbm.fr/).
All documentation is in the `docs` directory and online at [https://ae-utbm.github.io/sith3](https://ae-utbm.github.io/sith3). This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English. All documentation is in the `docs` directory and online at [https://ae-utbm.github.io/sith](https://ae-utbm.github.io/sith). This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.
#### If you want to contribute, here's how we recommend to read the docs: #### If you want to contribute, here's how we recommend to read the docs:

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

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

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -35,6 +35,7 @@ from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import localdate
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import Group, MetaGroup, Notification, Page, RealGroup, SithFile, User from core.models import Group, MetaGroup, Notification, Page, RealGroup, SithFile, User
@ -269,7 +270,7 @@ class Club(models.Model):
class MembershipQuerySet(models.QuerySet): class MembershipQuerySet(models.QuerySet):
def ongoing(self) -> Self: def ongoing(self) -> Self:
"""Filter all memberships which are not finished yet.""" """Filter all memberships which are not finished yet."""
return self.filter(Q(end_date=None) | Q(end_date__gt=timezone.now().date())) return self.filter(Q(end_date=None) | Q(end_date__gt=localdate()))
def board(self) -> Self: def board(self) -> Self:
"""Filter all memberships where the user is/was in the board. """Filter all memberships where the user is/was in the board.

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
@ -19,7 +19,7 @@ from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.timezone import localtime, now from django.utils.timezone import localdate, localtime, now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from club.forms import MailingForm from club.forms import MailingForm
@ -109,7 +109,7 @@ class TestMembershipQuerySet(TestClub):
def test_ongoing_with_membership_ending_today(self): def test_ongoing_with_membership_ending_today(self):
"""Test that a membership ending the present day is considered as ended.""" """Test that a membership ending the present day is considered as ended."""
today = timezone.now().date() today = localdate()
self.richard.memberships.filter(club=self.club).update(end_date=today) self.richard.memberships.filter(club=self.club).update(end_date=today)
current_members = list(self.club.members.ongoing().order_by("id")) current_members = list(self.club.members.ongoing().order_by("id"))
expected = [ expected = [

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

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

View File

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

View File

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

View File

@ -2,7 +2,9 @@
<html lang="fr"> <html lang="fr">
<head> <head>
<title>{% trans %}Slideshow{% endtrans %}</title> <title>{% trans %}Slideshow{% endtrans %}</title>
<link href="{{ static('com/css/slideshow.scss') }}" rel="stylesheet" type="text/css" /> <link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
<script src="{{ static('webpack/jquery-index.js') }}"></script>
<script src="{{ static('com/js/slideshow.js') }}"></script>
</head> </head>
<body> <body>
<div id="slideshow"> <div id="slideshow">
@ -10,7 +12,7 @@
<div id="slides"> <div id="slides">
{% for poster in posters %} {% for poster in posters %}
<div class="slide {% if loop.first %}center{% else %}right{% endif %}" display_time="{{ poster.display_time }}"> <div class="slide {% if loop.first %}center{% else %}right{% endif %}" display_time="{{ poster.display_time }}">
<img src="{{ poster.file.url }}"></img> <img src="{{ poster.file.url }}">
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -24,7 +26,5 @@
<div id="progress_bar"></div> <div id="progress_bar"></div>
</div> </div>
<script src="{{ static('core/js/jquery-3.6.2.min.js') }}"></script>
<script src="{{ static('com/js/slideshow.js') }}"></script>
</body> </body>
</html> </html>

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -34,6 +34,7 @@ from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.timezone import localdate
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, View from django.views.generic import DetailView, ListView, View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
@ -381,8 +382,8 @@ class NewsListView(CanViewMixin, ListView):
kwargs["timedelta"] = timedelta kwargs["timedelta"] = timedelta
kwargs["birthdays"] = ( kwargs["birthdays"] = (
User.objects.filter( User.objects.filter(
date_of_birth__month=timezone.now().month, date_of_birth__month=localdate().month,
date_of_birth__day=timezone.now().day, date_of_birth__day=localdate().day,
) )
.filter(role__in=["STUDENT", "FORMER STUDENT"]) .filter(role__in=["STUDENT", "FORMER STUDENT"])
.order_by("-date_of_birth") .order_by("-date_of_birth")

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

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

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -33,6 +33,7 @@ from django.core.management import call_command
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import connection from django.db import connection
from django.utils import timezone from django.utils import timezone
from django.utils.timezone import localdate
from PIL import Image from PIL import Image
from accounting.models import ( from accounting.models import (
@ -914,7 +915,7 @@ Welcome to the wiki page!
Membership( Membership(
user=comunity, user=comunity,
club=bar_club, club=bar_club,
start_date=self.now, start_date=localdate(),
role=settings.SITH_CLUB_ROLES_ID["Board member"], role=settings.SITH_CLUB_ROLES_ID["Board member"],
).save() ).save()
# Adding user tutu # Adding user tutu
@ -1274,28 +1275,28 @@ Welcome to the wiki page!
club=troll, club=troll,
role=9, role=9,
description="Padawan Troll", description="Padawan Troll",
start_date=self.now - timedelta(days=17), start_date=localdate() - timedelta(days=17),
).save() ).save()
Membership( Membership(
user=krophil, user=krophil,
club=troll, club=troll,
role=10, role=10,
description="Maitre Troll", description="Maitre Troll",
start_date=self.now - timedelta(days=200), start_date=localdate() - timedelta(days=200),
).save() ).save()
Membership( Membership(
user=skia, user=skia,
club=troll, club=troll,
role=2, role=2,
description="Grand Ancien Troll", description="Grand Ancien Troll",
start_date=self.now - timedelta(days=400), start_date=localdate() - timedelta(days=400),
end_date=self.now - timedelta(days=86), end_date=localdate() - timedelta(days=86),
).save() ).save()
Membership( Membership(
user=richard, user=richard,
club=troll, club=troll,
role=2, role=2,
description="", description="",
start_date=self.now - timedelta(days=200), start_date=localdate() - timedelta(days=200),
end_date=self.now - timedelta(days=100), end_date=localdate() - timedelta(days=100),
).save() ).save()

View File

@ -9,7 +9,7 @@ from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import Count, Exists, F, Min, OuterRef, Subquery, Sum from django.db.models import Count, Exists, F, Min, OuterRef, Subquery, Sum
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils.timezone import make_aware, now from django.utils.timezone import localdate, make_aware, now
from faker import Faker from faker import Faker
from club.models import Club, Membership from club.models import Club, Membership
@ -125,7 +125,7 @@ class Command(BaseCommand):
account_id=f"{9900 + i}{self.faker.random_lowercase_letter()}", account_id=f"{9900 + i}{self.faker.random_lowercase_letter()}",
) )
) )
while sub.subscription_end < now().date() and random.random() > 0.7: while sub.subscription_end < localdate() and random.random() > 0.7:
# 70% chances to subscribe again # 70% chances to subscribe again
# (expect if it would make the subscription start after tomorrow) # (expect if it would make the subscription start after tomorrow)
sub = prepare_subscription( sub = prepare_subscription(
@ -331,7 +331,7 @@ class Command(BaseCommand):
seller=random.choice(sellers), seller=random.choice(sellers),
customer=customer, customer=customer,
date=make_aware( date=make_aware(
self.faker.date_time_between(customer.since, now().date()) self.faker.date_time_between(customer.since, localdate())
), ),
) )
) )
@ -347,7 +347,7 @@ class Command(BaseCommand):
operator=random.choice(sellers), operator=random.choice(sellers),
customer=customer, customer=customer,
date=make_aware( date=make_aware(
self.faker.date_time_between(customer.since, now().date()) self.faker.date_time_between(customer.since, localdate())
), ),
is_validated=True, is_validated=True,
) )
@ -368,7 +368,7 @@ class Command(BaseCommand):
active_period_start = self.faker.past_date("-10y") active_period_start = self.faker.past_date("-10y")
active_period_end = self.faker.date_between( active_period_end = self.faker.date_between(
active_period_start, active_period_start,
min(now().date(), active_period_start + relativedelta(years=5)), min(localdate(), active_period_start + relativedelta(years=5)),
) )
for _ in range(nb_perms): for _ in range(nb_perms):
counter = ( counter = (

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -5,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -991,8 +991,8 @@ class SithFile(models.Model):
return user.is_board_member return user.is_board_member
if user.is_com_admin: if user.is_com_admin:
return True return True
if self.is_in_sas: if self.is_in_sas and user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
return user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) return True
return user.id == self.owner_id return user.id == self.owner_id
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user):

View File

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

View File

@ -1,59 +1,66 @@
$( function() { $(() => {
buttons = $(".choose_file_button"); // const buttons = $('.choose_file_button')
popups = $(".choose_file_widget"); const popups = $(".choose_file_widget");
popups.dialog({ popups.dialog({
autoOpen: false, autoOpen: false,
modal: true, modal: true,
width: "90%", width: "90%",
create: function (event) { create: (event) => {
target = $(event.target); const target = $(event.target);
target.parent().css({ target.parent().css({
'position': 'fixed', position: "fixed",
'top': '5%', top: "5%",
'bottom': '5%', bottom: "5%",
}); });
target.css("height", "300px"); target.css("height", "300px");
console.log(target);
}, },
buttons: [ buttons: [
{ {
text: "Choose", text: "Choose",
click: function() { click: function () {
console.log($("#file_id")); $(`input[name=${$(this).attr("name")}]`).attr(
$("input[name="+$(this).attr('name')+"]").attr('value', $("#file_id").attr('value')); "value",
$( this ).dialog( "close" ); $("#file_id").attr("value"),
);
$(this).dialog("close");
}, },
disabled: true, disabled: true,
} },
], ],
}); });
$( ".choose_file_button" ).button().on( "click", function() { $(".choose_file_button")
popup = popups.filter("[name="+$(this).attr('name')+"]"); .button()
console.log(popup); .on("click", function () {
popup.html('<iframe src="/file/popup" width="100%" height="95%"></iframe><div id="file_id" value="null" />'); const popup = popups.filter(`[name=${$(this).attr("name")}]`);
popup.dialog({title: $(this).text()}).dialog( "open" ); 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 () { $("#quick_notif li").click(function () {
$(this).hide(); $(this).hide();
}) });
}); });
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function createQuickNotif(msg) { function createQuickNotif(msg) {
const el = document.createElement('li') const el = document.createElement("li");
el.textContent = msg el.textContent = msg;
el.addEventListener('click', () => el.parentNode.removeChild(el)) el.addEventListener("click", () => el.parentNode.removeChild(el));
document.getElementById('quick_notif').appendChild(el) document.getElementById("quick_notif").appendChild(el);
} }
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function deleteQuickNotifs() { function deleteQuickNotifs() {
const el = document.getElementById('quick_notif') const el = document.getElementById("quick_notif");
while (el.firstChild) { while (el.firstChild) {
el.removeChild(el.firstChild) el.removeChild(el.firstChild);
} }
} }
function display_notif() { // biome-ignore lint/correctness/noUnusedVariables: used in other scripts
$('#header_notif').toggle().parent().toggleClass("white"); function displayNotif() {
$("#header_notif").toggle().parent().toggleClass("white");
} }
// You can't get the csrf token from the template in a widget // 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 // 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 // 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 // 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() { function getCSRFToken() {
return $("[name=csrfmiddlewaretoken]").val(); return $("[name=csrfmiddlewaretoken]").val();
} }
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
const initialUrlParams = new URLSearchParams(window.location.search); const initialUrlParams = new URLSearchParams(window.location.search);
/** /**
@ -74,8 +83,11 @@ const initialUrlParams = new URLSearchParams(window.location.search);
* @enum {number} * @enum {number}
*/ */
const History = { const History = {
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
NONE: 0, NONE: 0,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
PUSH: 1, PUSH: 1,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
REPLACE: 2, REPLACE: 2,
}; };
@ -85,58 +97,29 @@ const History = {
* @param {History} action * @param {History} action
* @param {URL | null} url * @param {URL | null} url
*/ */
function update_query_string(key, value, action = History.REPLACE, url = null) { // biome-ignore lint/correctness/noUnusedVariables: used in other scripts
if (!url){ function updateQueryString(key, value, action = History.REPLACE, url = null) {
url = new URL(window.location.href); let ret = url;
if (!ret) {
ret = new URL(window.location.href);
} }
if (value === undefined || value === null || value === "") { if (value === undefined || value === null || value === "") {
// If the value is null, undefined or empty => delete it // If the value is null, undefined or empty => delete it
url.searchParams.delete(key) ret.searchParams.delete(key);
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
url.searchParams.delete(key) ret.searchParams.delete(key);
value.forEach((v) => url.searchParams.append(key, v)) for (const v of value) {
ret.searchParams.append(key, v);
}
} else { } else {
url.searchParams.set(key, value); ret.searchParams.set(key, value);
} }
if (action === History.PUSH) { if (action === History.PUSH) {
history.pushState(null, "", url.toString()); window.history.pushState(null, "", ret.toString());
} else if (action === History.REPLACE) { } 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
// please test this function. A all cost.
/**
* Given a paginated endpoint, fetch all the items of this endpoint,
* performing multiple API calls if necessary.
* @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");
let first_page = (await ( await fetch(paginated_url)).json());
let results = first_page.results;
const nb_pictures = first_page.count
const nb_pages = Math.ceil(nb_pictures / max_per_page);
if (nb_pages > 1) {
let promises = [];
for (let i = 2; i <= nb_pages; i++) {
paginated_url.searchParams.set("page", i.toString());
promises.push(
fetch(paginated_url).then(res => res.json().then(json => json.results))
);
}
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 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // 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({ * document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"), * element: document.getElementById("select2-input"),
* data_source: local_data_source(data) * dataSource: localDataSource(data)
* })); * }));
* ``` * ```
* *
@ -29,7 +29,7 @@
* ]; * ];
* document.addEventListener("DOMContentLoaded", () => sithSelect2({ * document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"), * 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)) * excluded: () => data.filter((i) => i.text === "to exclude").map((i) => parseInt(i))
* }) * })
* })); * }));
@ -38,15 +38,15 @@
* # Remote data source * # Remote data source
* *
* Select2 with remote data sources are similar to those with local * 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`. * which takes a callback that must return a `Select2Object`.
* *
* ```js * ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({ * document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"), * 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 * 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 * ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({ * document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"), * element: document.getElementById("select2-input"),
* data_source: remote_data_source("/api/user/search", { * dataSource: remoteDataSource("/api/user/search", {
* result_converter: (user) => Object({id: user.id, text: user.first_name}), * resultConverter: (user) => Object({id: user.id, text: user.firstName}),
* overrides: { * overrides: {
* delay: 500 * delay: 500
* } * }
@ -85,15 +85,15 @@
* *
* Sometimes, you would like to display an image besides * Sometimes, you would like to display an image besides
* the text on the select items. * the text on the select items.
* In this case, fill the `picture_getter` option : * In this case, fill the `pictureGetter` option :
* *
* ```js * ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({ * document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"), * element: document.getElementById("select2-input"),
* data_source: remote_data_source("/api/user/search", { * dataSource: remoteDataSource("/api/user/search", {
* result_converter: (user) => Object({id: user.id, text: user.first_name}) * resultConverter: (user) => Object({id: user.id, text: user.firstName})
* }) * })
* picture_getter: (user) => user.profile_pict, * pictureGetter: (user) => user.profilePict,
* })); * }));
* ``` * ```
* *
@ -105,8 +105,8 @@
* <body> * <body>
* <div x-data="select2_test"> * <div x-data="select2_test">
* <select x-ref="search" x-ref="select"></select> * <select x-ref="search" x-ref="select"></select>
* <p x-text="current_selection.id"></p> * <p x-text="currentSelection.id"></p>
* <p x-text="current_selection.text"></p> * <p x-text="currentSelection.text"></p>
* </div> * </div>
* </body> * </body>
* *
@ -114,20 +114,20 @@
* document.addEventListener("alpine:init", () => { * document.addEventListener("alpine:init", () => {
* Alpine.data("select2_test", () => ({ * Alpine.data("select2_test", () => ({
* selector: undefined, * selector: undefined,
* current_select: {id: "", text: ""}, * currentSelect: {id: "", text: ""},
* *
* init() { * init() {
* this.selector = sithSelect2({ * this.selector = sithSelect2({
* element: $(this.$refs.select), * element: $(this.$refs.select),
* data_source: local_data_source( * dataSource: localDataSource(
* [{id: 1, text: "foo"}, {id: 2, text: "bar"}] * [{id: 1, text: "foo"}, {id: 2, text: "bar"}]
* ), * ),
* }); * });
* this.selector.on("select2:select", (event) => { * this.selector.on("select2:select", (event) => {
* // select2 => Alpine signals here * // 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 * // Alpine => select2 signals here
* }); * });
* }, * },
@ -145,10 +145,10 @@
/** /**
* @typedef Select2Options * @typedef Select2Options
* @property {Element} element * @property {Element} element
* @property {Object} data_source * @property {Object} dataSource
* the data source, built with `local_data_source` or `remote_data_source` * the data source, built with `localDataSource` or `remoteDataSource`
* @property {number[]} excluded A list of ids to exclude from search * @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 * A callback to get the picture field from the API response
* @property {Object | undefined} overrides * @property {Object | undefined} overrides
* Any other select2 parameter to apply on the config * Any other select2 parameter to apply on the config
@ -157,13 +157,14 @@
/** /**
* @param {Select2Options} options * @param {Select2Options} options
*/ */
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function sithSelect2(options) { function sithSelect2(options) {
const elem = $(options.element); const elem = $(options.element);
return elem.select2({ return elem.select2({
theme: elem[0].multiple ? "classic" : "default", theme: elem[0].multiple ? "classic" : "default",
minimumInputLength: 2, minimumInputLength: 2,
templateResult: select_item_builder(options.picture_getter), templateResult: selectItemBuilder(options.pictureGetter),
...options.data_source, ...options.dataSource,
...(options.overrides || {}), ...(options.overrides || {}),
}); });
} }
@ -179,8 +180,9 @@ function sithSelect2(options) {
* @param {Select2Object[]} source The array containing the data * @param {Select2Object[]} source The array containing the data
* @param {RemoteSourceOptions} options * @param {RemoteSourceOptions} options
*/ */
function local_data_source(source, options) { // biome-ignore lint/correctness/noUnusedVariables: used in other scripts
if (!!options.excluded) { function localDataSource(source, options) {
if (options.excluded) {
const ids = options.excluded(); const ids = options.excluded();
return { data: source.filter((i) => !ids.includes(i.id)) }; return { data: source.filter((i) => !ids.includes(i.id)) };
} }
@ -191,7 +193,7 @@ function local_data_source(source, options) {
* @typedef RemoteSourceOptions * @typedef RemoteSourceOptions
* @property {undefined | function(): number[]} excluded * @property {undefined | function(): number[]} excluded
* A callback to the ids to exclude from the search * 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 * A converter for a value coming from the remote api
* @property {undefined | Object} overrides * @property {undefined | Object} overrides
* Any other select2 parameter to apply on the config * 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 {string} source The url of the endpoint
* @param {RemoteSourceOptions} options * @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; jQuery.ajaxSettings.traditional = true;
let params = { const params = {
url: source, url: source,
dataType: "json", dataType: "json",
cache: true, cache: true,
@ -213,24 +217,25 @@ function remote_data_source(source, options) {
return { return {
search: params.term, search: params.term,
exclude: [ exclude: [
...(this.val() || []).map((i) => parseInt(i)), ...(this.val() || []).map((i) => Number.parseInt(i)),
...(options.excluded ? options.excluded() : []), ...(options.excluded ? options.excluded() : []),
], ],
}; };
}, },
}; };
if (!!options.result_converter) { if (options.resultConverter) {
params["processResults"] = function (data) { params.processResults = (data) => ({
return { results: data.results.map(options.result_converter) }; results: data.results.map(options.resultConverter),
}; });
} }
if (!!options.overrides) { if (options.overrides) {
Object.assign(params, options.overrides); Object.assign(params, options.overrides);
} }
return { ajax: params }; return { ajax: params };
} }
function item_formatter(user) { // biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function itemFormatter(user) {
if (user.loading) { if (user.loading) {
return user.text; return user.text;
} }
@ -238,23 +243,22 @@ function item_formatter(user) {
/** /**
* Build a function to display the results * 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} * @return {function(string): jQuery|HTMLElement}
*/ */
function select_item_builder(picture_getter) { function selectItemBuilder(pictureGetter) {
return (item) => { return (item) => {
const picture = const picture = typeof pictureGetter === "function" ? pictureGetter(item) : null;
typeof picture_getter === "function" ? picture_getter(item) : null; const imgHtml = picture
const img_html = picture
? `<img ? `<img
src="${picture_getter(item)}" src="${pictureGetter(item)}"
alt="${item.text}" alt="${item.text}"
onerror="this.src = '/static/core/img/unknown.jpg'" onerror="this.src = '/static/core/img/unknown.jpg'"
/>` />`
: ""; : "";
return $(`<div class="select-item"> return $(`<div class="select-item">
${img_html} ${imgHtml}
<span class="select-item-text">${item.text}</span> <span class="select-item-text">${item.text}</span>
</div>`); </div>`);
}; };

View File

@ -228,6 +228,15 @@ a:not(.button) {
} }
} }
.edit-action {
color: orange;
}
.delete-action {
color: red;
}
#page { #page {
width: 90%; width: 90%;
margin: 20px auto 0; margin: 20px auto 0;

View File

@ -1,273 +0,0 @@
async function get_graph_data(url, godfathers_depth, godchildren_depth) {
let data = await (
await fetch(
`${url}?godfathers_depth=${godfathers_depth}&godchildren_depth=${godchildren_depth}`,
)
).json();
return [
...data.users.map((user) => {
return { data: user };
}),
...data.relationships.map((rel) => {
return {
data: { source: rel.godfather, target: rel.godchild },
};
}),
];
}
function create_graph(container, data, active_user_id) {
let cy = cytoscape({
boxSelectionEnabled: false,
autounselectify: true,
container: container,
elements: data,
minZoom: 0.5,
style: [
// the stylesheet for the graph
{
selector: "node",
style: {
label: "data(display_name)",
"background-image": "data(profile_pict)",
width: "100%",
height: "100%",
"background-fit": "cover",
"background-repeat": "no-repeat",
shape: "ellipse",
},
},
{
selector: "edge",
style: {
width: 5,
"line-color": "#ccc",
"target-arrow-color": "#ccc",
"target-arrow-shape": "triangle",
"curve-style": "bezier",
},
},
{
selector: ".traversed",
style: {
"border-width": "5px",
"border-style": "solid",
"border-color": "red",
"target-arrow-color": "red",
"line-color": "red",
},
},
{
selector: ".not-traversed",
style: {
"line-opacity": "0.5",
"background-opacity": "0.5",
"background-image-opacity": "0.5",
},
},
],
layout: {
name: "klay",
nodeDimensionsIncludeLabels: true,
fit: true,
klay: {
addUnnecessaryBendpoints: true,
direction: "DOWN",
nodePlacement: "INTERACTIVE",
layoutHierarchy: true,
},
},
});
let active_user = cy
.getElementById(active_user_id)
.style("shape", "rectangle");
/* Reset graph */
let reset_graph = () => {
cy.elements((element) => {
if (element.hasClass("traversed")) {
element.removeClass("traversed");
}
if (element.hasClass("not-traversed")) {
element.removeClass("not-traversed");
}
});
};
let on_node_tap = (el) => {
reset_graph();
/* Create path on graph if selected isn't the targeted user */
if (el === active_user) {
return;
}
cy.elements((element) => {
element.addClass("not-traversed");
});
cy.elements()
.aStar({
root: el,
goal: active_user,
})
.path.forEach((el) => {
el.removeClass("not-traversed");
el.addClass("traversed");
});
};
cy.on("tap", "node", (tapped) => {
on_node_tap(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;
}
cy.cxtmenu({
selector: "node",
commands: [
{
content: '<i class="fa fa-external-link fa-2x"></i>',
select: function (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);
},
},
{
content: '<i class="fa fa-eraser fa-2x"></i>',
select: function (el) {
reset_graph();
},
},
],
});
return cy;
}
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
*/
const default_depth = 2;
if (
typeof api_url === "undefined" ||
typeof active_user === "undefined" ||
typeof depth_min === "undefined" ||
typeof depth_max === "undefined"
) {
console.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;
}
return value;
}
Alpine.data("graph", () => ({
loading: false,
godfathers_depth: get_initial_depth("godfathers_depth"),
godchildren_depth: get_initial_depth("godchildren_depth"),
reverse: initialUrlParams.get("reverse")?.toLowerCase?.() === "true",
graph: undefined,
graph_data: {},
async init() {
let delayed_fetch = Alpine.debounce(async () => {
this.fetch_graph_data();
}, 100);
["godfathers_depth", "godchildren_depth"].forEach((param) => {
this.$watch(param, async (value) => {
if (value < depth_min || value > depth_max) {
return;
}
update_query_string(param, value, History.REPLACE);
delayed_fetch();
});
});
this.$watch("reverse", async (value) => {
update_query_string("reverse", value, History.REPLACE);
this.reverse_graph();
});
this.$watch("graph_data", async () => {
await this.generate_graph();
if (this.reverse) {
await this.reverse_graph();
}
});
this.fetch_graph_data();
},
async screenshot() {
const link = document.createElement("a");
link.href = this.graph.jpg();
link.download = interpolate(
gettext("family_tree.%(extension)s"),
{ extension: "jpg" },
true,
);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
async reset() {
this.reverse = false;
this.godfathers_depth = default_depth;
this.godchildren_depth = default_depth;
},
async reverse_graph() {
this.graph.elements((el) => {
el.position(new Object({ 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 generate_graph() {
this.loading = true;
this.graph = create_graph(
$(this.$refs.graph),
this.graph_data,
active_user,
);
this.loading = false;
},
}));
});

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 434 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Jimmy Wärting
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE 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.

View File

@ -1 +0,0 @@
export*from"./src/es6.js";

View File

@ -1 +0,0 @@
import FileSystemHandle from"./FileSystemHandle.js";import{errors}from"./util.js";const{GONE:GONE,MOD_ERR:MOD_ERR}=errors,kAdapter=Symbol("adapter");class FileSystemDirectoryHandle extends FileSystemHandle{[kAdapter];constructor(e){super(e),this[kAdapter]=e}async getDirectoryHandle(e,t={}){if(""===e)throw new TypeError("Name can't be an empty string.");if("."===e||".."===e||e.includes("/"))throw new TypeError("Name contains invalid characters.");t.create=!!t.create;const r=await this[kAdapter].getDirectoryHandle(e,t);return new FileSystemDirectoryHandle(r)}async*entries(){const{FileSystemFileHandle:e}=await import("./FileSystemFileHandle.js");for await(const[t,r]of this[kAdapter].entries())yield[r.name,"file"===r.kind?new e(r):new FileSystemDirectoryHandle(r)]}async*getEntries(){const{FileSystemFileHandle:e}=await import("./FileSystemFileHandle.js");console.warn("deprecated, use .entries() instead");for await(let t of this[kAdapter].entries())yield"file"===t.kind?new e(t):new FileSystemDirectoryHandle(t)}async getFileHandle(e,t={}){const{FileSystemFileHandle:r}=await import("./FileSystemFileHandle.js");if(""===e)throw new TypeError("Name can't be an empty string.");if("."===e||".."===e||e.includes("/"))throw new TypeError("Name contains invalid characters.");t.create=!!t.create;return new r(await this[kAdapter].getFileHandle(e,t))}async removeEntry(e,t={}){if(""===e)throw new TypeError("Name can't be an empty string.");if("."===e||".."===e||e.includes("/"))throw new TypeError("Name contains invalid characters.");return t.recursive=!!t.recursive,this[kAdapter].removeEntry(e,t)}async resolve(e){if(await e.isSameEntry(this))return[];const t=[{handle:this,path:[]}];for(;t.length;){let{handle:r,path:n}=t.pop();for await(const a of r.values()){if(await a.isSameEntry(e))return[...n,a.name];"directory"===a.kind&&t.push({handle:a,path:[...n,a.name]})}}return null}async*keys(){for await(const[e]of this[kAdapter].entries())yield e}async*values(){for await(const[e,t]of this)yield t}[Symbol.asyncIterator](){return this.entries()}}if(Object.defineProperty(FileSystemDirectoryHandle.prototype,Symbol.toStringTag,{value:"FileSystemDirectoryHandle",writable:!1,enumerable:!1,configurable:!0}),Object.defineProperties(FileSystemDirectoryHandle.prototype,{getDirectoryHandle:{enumerable:!0},entries:{enumerable:!0},getFileHandle:{enumerable:!0},removeEntry:{enumerable:!0}}),globalThis.FileSystemDirectoryHandle){const e=globalThis.FileSystemDirectoryHandle.prototype;async function ensureDoActuallyStillExist(e){const t=await navigator.storage.getDirectory();if(null===await t.resolve(e))throw new DOMException(...GONE)}e.resolve=async function(e){if(await e.isSameEntry(this))return[];const t=[{handle:this,path:[]}];for(;t.length;){let{handle:r,path:n}=t.pop();for await(const a of r.values()){if(await a.isSameEntry(e))return[...n,a.name];"directory"===a.kind&&t.push({handle:a,path:[...n,a.name]})}}return null};const t=e.entries;e.entries=async function*(){await ensureDoActuallyStillExist(this),yield*t.call(this)},e[Symbol.asyncIterator]=async function*(){yield*this.entries()};const r=e.removeEntry;e.removeEntry=async function(e,n={}){return r.call(this,e,n).catch((async e=>{if(e instanceof DOMException&&"UnknownError"===e.name&&!n.recursive){if(!(await t.call(this).next()).done)throw new DOMException(...MOD_ERR)}throw e}))}}export default FileSystemDirectoryHandle;export{FileSystemDirectoryHandle};

View File

@ -1 +0,0 @@
import FileSystemHandle from"./FileSystemHandle.js";import FileSystemWritableFileStream from"./FileSystemWritableFileStream.js";import{errors}from"./util.js";const{INVALID:INVALID,SYNTAX:SYNTAX,GONE:GONE}=errors,kAdapter=Symbol("adapter");class FileSystemFileHandle extends FileSystemHandle{[kAdapter];constructor(e){super(e),this[kAdapter]=e}async createWritable(e={}){return new FileSystemWritableFileStream(await this[kAdapter].createWritable(e))}async getFile(){return this[kAdapter].getFile()}}if(Object.defineProperty(FileSystemFileHandle.prototype,Symbol.toStringTag,{value:"FileSystemFileHandle",writable:!1,enumerable:!1,configurable:!0}),Object.defineProperties(FileSystemFileHandle.prototype,{createWritable:{enumerable:!0},getFile:{enumerable:!0}}),globalThis.FileSystemFileHandle&&!globalThis.FileSystemFileHandle.prototype.createWritable){const e=new WeakMap;let t;const a=()=>{let e,t;onmessage=async a=>{const i=a.ports[0],r=a.data;switch(r.type){case"open":const a=r.name;let i=await navigator.storage.getDirectory();for(const e of r.path)i=await i.getDirectoryHandle(e);e=await i.getFileHandle(a),t=await e.createSyncAccessHandle();break;case"write":t.write(r.data,{at:r.position}),t.flush();break;case"truncate":t.truncate(r.size);break;case"abort":case"close":t.close()}i.postMessage(0)}};globalThis.FileSystemFileHandle.prototype.createWritable=async function(i){if(!t){const e=`(${a.toString()})()`,i=new Blob([e],{type:"text/javascript"});t=URL.createObjectURL(i)}const r=new Worker(t,{type:"module"});let n=0;const s=new TextEncoder;let o=await this.getFile().then((e=>e.size));const l=e=>new Promise(((t,a)=>{const i=new MessageChannel;i.port1.onmessage=e=>{e.data instanceof Error?a(e.data):t(e.data),i.port1.close(),i.port2.close(),i.port1.onmessage=null},r.postMessage(e,[i.port2])})),c=await navigator.storage.getDirectory(),p=await e.get(this),y=await c.resolve(p);if(null===y)throw new DOMException(...GONE);let d;await l({type:"open",path:y,name:this.name}),!1===i?.keepExistingData&&(await l({type:"truncate",size:0}),o=0);return new FileSystemWritableFileStream({start:e=>{d=e},async write(e){if("write"===(e=e?.constructor===Object?{...e}:{type:"write",data:e,position:n}).type){if(!("data"in e))throw await l({type:"close"}),new DOMException(...SYNTAX("write requires a data argument"));if(e.position??=n,"string"==typeof e.data)e.data=s.encode(e.data);else if(e.data instanceof ArrayBuffer)e.data=new Uint8Array(e.data);else if(e.data instanceof Uint8Array||!ArrayBuffer.isView(e.data)){if(!(e.data instanceof Uint8Array)){const t=await new Response(e.data).arrayBuffer();e.data=new Uint8Array(t)}}else e.data=new Uint8Array(e.data.buffer,e.data.byteOffset,e.data.byteLength);Number.isInteger(e.position)&&e.position>=0&&(n=e.position),n+=e.data.byteLength,o+=e.data.byteLength}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(o<e.position)throw new DOMException(...INVALID);return console.log("seeking",e),void(n=e.position)}throw await l({type:"close"}),new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===e.type){if(!(Number.isInteger(e.size)&&e.size>=0))throw await l({type:"close"}),new DOMException(...SYNTAX("truncate requires a size argument"));o=e.size,n>o&&(n=o)}}await l(e)},async close(){await l({type:"close"}),r.terminate()},async abort(e){await l({type:"abort",reason:e}),r.terminate()}})};const i=FileSystemDirectoryHandle.prototype.getFileHandle;FileSystemDirectoryHandle.prototype.getFileHandle=async function(...t){const a=await i.call(this,...t);return e.set(a,this),a}}export default FileSystemFileHandle;export{FileSystemFileHandle};

View File

@ -1 +0,0 @@
const kAdapter=Symbol("adapter");class FileSystemHandle{[kAdapter];name;kind;constructor(e){this.kind=e.kind,this.name=e.name,this[kAdapter]=e}async queryPermission(e={}){const{mode:r="read"}=e,t=this[kAdapter];if(t.queryPermission)return t.queryPermission({mode:r});if("read"===r)return"granted";if("readwrite"===r)return t.writable?"granted":"denied";throw new TypeError(`Mode ${r} must be 'read' or 'readwrite'`)}async requestPermission({mode:e="read"}={}){const r=this[kAdapter];if(r.requestPermission)return r.requestPermission({mode:e});if("read"===e)return"granted";if("readwrite"===e)return r.writable?"granted":"denied";throw new TypeError(`Mode ${e} must be 'read' or 'readwrite'`)}async remove(e={}){await this[kAdapter].remove(e)}async isSameEntry(e){return this===e||!(!e||"object"!=typeof e||this.kind!==e.kind||!e[kAdapter])&&this[kAdapter].isSameEntry(e[kAdapter])}}Object.defineProperty(FileSystemHandle.prototype,Symbol.toStringTag,{value:"FileSystemHandle",writable:!1,enumerable:!1,configurable:!0}),globalThis.FileSystemHandle&&(globalThis.FileSystemHandle.prototype.queryPermission??=function(e){return"granted"});export default FileSystemHandle;export{FileSystemHandle};

View File

@ -1 +0,0 @@
import config from"./config.js";const{WritableStream:WritableStream}=config;class FileSystemWritableFileStream extends WritableStream{#e;constructor(e){super(e),this.#e=e,Object.setPrototypeOf(this,FileSystemWritableFileStream.prototype),this._closed=!1}async close(){this._closed=!0;const e=this.getWriter(),t=e.close();return e.releaseLock(),t}seek(e){return this.write({type:"seek",position:e})}truncate(e){return this.write({type:"truncate",size:e})}write(e){if(this._closed)return Promise.reject(new TypeError("Cannot write to a CLOSED writable stream"));const t=this.getWriter(),r=t.write(e);return t.releaseLock(),r}}Object.defineProperty(FileSystemWritableFileStream.prototype,Symbol.toStringTag,{value:"FileSystemWritableFileStream",writable:!1,enumerable:!1,configurable:!0}),Object.defineProperties(FileSystemWritableFileStream.prototype,{close:{enumerable:!0},seek:{enumerable:!0},truncate:{enumerable:!0},write:{enumerable:!0}}),!globalThis.FileSystemFileHandle||globalThis.FileSystemFileHandle.prototype.createWritable||globalThis.FileSystemWritableFileStream||(globalThis.FileSystemWritableFileStream=FileSystemWritableFileStream);export default FileSystemWritableFileStream;export{FileSystemWritableFileStream};

View File

@ -1 +0,0 @@
import{errors}from"../util.js";const{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX,SECURITY:SECURITY,DISALLOWED:DISALLOWED}=errors;export class Sink{constructor(){}write(e){}close(){}}export class FileHandle{constructor(){this._path=""}async getFile(){return new File([],"")}async createWritable(){}async isSameEntry(e){return e._path===this._path}}export class FolderHandle{constructor(){this._path=""}async*entries(){yield}async isSameEntry(e){return e._path===this._path}async getDirectoryHandle(e,r){return new FolderHandle}async getFileHandle(e,r){return new FileHandle}async removeEntry(e,r){}}const fs=new FolderHandle("");export default()=>fs;

View File

@ -1 +0,0 @@
import{errors}from"../util.js";const{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX}=errors,DIR={headers:{"content-type":"dir"}},FILE=()=>({headers:{"content-type":"file","last-modified":Date.now()}}),hasOwn=Object.prototype.hasOwnProperty;class Sink{constructor(e,t,i){this._cache=e,this.path=t,this.size=i.size,this.position=0,this.file=i}write(e,t){if("object"==typeof e)if("write"===e.type){if(Number.isInteger(e.position)&&e.position>=0&&(this.size<e.position&&(this.file=new Blob([this.file,new ArrayBuffer(e.position-this.size)])),this.position=e.position),!("data"in e))throw new DOMException(...SYNTAX("write requires a data argument"));e=e.data}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(this.size<e.position)throw new DOMException(...INVALID);return void(this.position=e.position)}throw new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===e.type){if(Number.isInteger(e.size)&&e.size>=0){let t=this.file;return t=e.size<this.size?t.slice(0,e.size):new File([t,new Uint8Array(e.size-this.size)],t.name),this.size=t.size,this.position>t.size&&(this.position=t.size),void(this.file=t)}throw new DOMException(...SYNTAX("truncate requires a size argument"))}}e=new Blob([e]);let i=this.file;const s=i.slice(0,this.position),n=i.slice(this.position+e.size);let a=this.position-s.size;a<0&&(a=0),i=new File([s,new Uint8Array(a),e,n],i.name),this.size=i.size,this.position+=e.size,this.file=i}async close(){const[e]=await this._cache.keys(this.path);if(!e)throw new DOMException(...GONE);return this._cache.put(this.path,new Response(this.file,FILE()))}}export class FileHandle{constructor(e,t){this._cache=t,this.path=e,this.kind="file",this.writable=!0,this.readable=!0}get name(){return this.path.split("/").pop()}async isSameEntry(e){return this.path===e.path}async getFile(){const e=await this._cache.match(this.path);if(!e)throw new DOMException(...GONE);const t=await e.blob();return new File([t],this.name,{lastModified:+e.headers.get("last-modified")})}async createWritable(e){const[t]=await this._cache.keys(this.path);if(!t)throw new DOMException(...GONE);return new Sink(this._cache,this.path,e.keepExistingData?await this.getFile():new File([],this.name))}}export class FolderHandle{constructor(e,t){this._dir=e,this.writable=!0,this.readable=!0,this._cache=t,this.kind="directory",this.name=e.split("/").pop()}async*entries(){for(const[e,t]of Object.entries(await this._tree))yield[e.split("/").pop(),t?new FileHandle(e,this._cache):new FolderHandle(e,this._cache)]}async isSameEntry(e){return this._dir===e._dir}async getDirectoryHandle(e,t){const i=this._dir.endsWith("/")?this._dir+e:`${this._dir}/${e}`,s=await this._tree;if(hasOwn.call(s,i)){if(s[i])throw new DOMException(...MISMATCH);return new FolderHandle(i,this._cache)}if(t.create)return s[i]=!1,await this._cache.put(i,new Response("{}",DIR)),await this._save(s),new FolderHandle(i,this._cache);throw new DOMException(...GONE)}get _tree(){return this._cache.match(this._dir).then((e=>e.json())).catch((e=>{throw new DOMException(...GONE)}))}_save(e){return this._cache.put(this._dir,new Response(JSON.stringify(e),DIR))}async getFileHandle(e,t){const i=this._dir.endsWith("/")?this._dir+e:`${this._dir}/${e}`,s=await this._tree;if(hasOwn.call(s,i)){if(!s[i])throw new DOMException(...MISMATCH);return new FileHandle(i,this._cache)}if(t.create){const e=await this._tree;return e[i]=!0,await this._cache.put(i,new Response("",FILE())),await this._save(e),new FileHandle(i,this._cache)}throw new DOMException(...GONE)}async removeEntry(e,t){const i=await this._tree,s=this._dir.endsWith("/")?this._dir+e:`${this._dir}/${e}`;if(!hasOwn.call(i,s))throw new DOMException(...GONE);if(t.recursive){const e=[...Object.entries(i)];for(;e.length;){const[t,i]=e.pop();if(i)await this._cache.delete(t);else{const i=await this._cache.match(t).then((e=>e.json()));e.push(...Object.entries(i))}}delete i[s]}else{const e=i[s];if(delete i[s],e)await this._cache.delete(s);else{const e=await this._cache.match(s).then((e=>e.json()));if(Object.keys(e).length)throw new DOMException(...MOD_ERR);await this._cache.delete(s)}}await this._save(i)}}export default async function(){const e=await caches.open("sandboxed-fs");return await e.match("/")||await e.put("/",new Response("{}",DIR)),new FolderHandle(location.origin+"/",e)}

View File

@ -1 +0,0 @@
import{join,basename}from"https://deno.land/std@0.108.0/path/mod.ts";import{errors}from"../util.js";const{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX}=errors;async function fileFrom(t){const e=Deno.readFileSync(t),i=await Deno.stat(t);return new File([e],basename(t),{lastModified:Number(i.mtime)})}export class Sink{constructor(t,e){this.fileHandle=t,this.size=e,this.position=0}async abort(){await this.fileHandle.close()}async write(t){if("object"==typeof t)if("write"===t.type){if(Number.isInteger(t.position)&&t.position>=0&&(this.position=t.position),!("data"in t))throw await this.fileHandle.close(),new DOMException(...SYNTAX("write requires a data argument"));t=t.data}else{if("seek"===t.type){if(Number.isInteger(t.position)&&t.position>=0){if(this.size<t.position)throw new DOMException(...INVALID);return void(this.position=t.position)}throw await this.fileHandle.close(),new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===t.type){if(Number.isInteger(t.size)&&t.size>=0)return await this.fileHandle.truncate(t.size),this.size=t.size,void(this.position>this.size&&(this.position=this.size));throw await this.fileHandle.close(),new DOMException(...SYNTAX("truncate requires a size argument"))}}if(t instanceof ArrayBuffer)t=new Uint8Array(t);else if("string"==typeof t)t=(new TextEncoder).encode(t);else if(t instanceof Blob){await this.fileHandle.seek(this.position,Deno.SeekMode.Start);for await(const e of t.stream()){const t=await this.fileHandle.write(e);this.position+=t,this.size+=t}return}await this.fileHandle.seek(this.position,Deno.SeekMode.Start);const e=await this.fileHandle.write(t);this.position+=e,this.size+=e}async close(){await this.fileHandle.close()}}export class FileHandle{#t;constructor(t,e){this.#t=t,this.name=e,this.kind="file"}async getFile(){return await Deno.stat(this.#t).catch((t=>{if("NotFound"===t.name)throw new DOMException(...GONE)})),fileFrom(this.#t)}async isSameEntry(t){return this.#t===this.#e.apply(t)}#e(){return this.#t}async createWritable(t){const e=await Deno.open(this.#t,{write:!0,truncate:!t.keepExistingData}).catch((t=>{if("NotFound"===t.name)throw new DOMException(...GONE);throw t})),{size:i}=await e.stat();return new Sink(e,i)}}export class FolderHandle{#t="";constructor(t,e=""){this.name=e,this.kind="directory",this.#t=join(t)}async isSameEntry(t){return this.#t===this.#e.apply(t)}#e(){return this.#t}async*entries(){const t=this.#t;try{for await(const e of Deno.readDir(t)){const{name:i}=e,n=join(t,i),o=await Deno.lstat(n);o.isFile?yield[i,new FileHandle(n,i)]:o.isDirectory&&(yield[i,new FolderHandle(n,i)])}}catch(t){throw"NotFound"===t.name?new DOMException(...GONE):t}}async getDirectoryHandle(t,e){const i=join(this.#t,t),n=await Deno.lstat(i).catch((t=>{if("NotFound"!==t.name)throw t})),o=n?.isDirectory;if(n&&o)return new FolderHandle(i,t);if(n&&!o)throw new DOMException(...MISMATCH);if(!e.create)throw new DOMException(...GONE);return await Deno.mkdir(i),new FolderHandle(i,t)}async getFileHandle(t,e){const i=join(this.#t,t),n=await Deno.lstat(i).catch((t=>{if("NotFound"!==t.name)throw t})),o=n?.isFile;if(n&&o)return new FileHandle(i,t);if(n&&!o)throw new DOMException(...MISMATCH);if(!e.create)throw new DOMException(...GONE);return(await Deno.open(i,{create:!0,write:!0})).close(),new FileHandle(i,t)}async queryPermission(){return"granted"}async removeEntry(t,e){const i=join(this.#t,t);(await Deno.lstat(i).catch((t=>{if("NotFound"===t.name)throw new DOMException(...GONE);throw t}))).isDirectory?e.recursive?await Deno.remove(i,{recursive:!0}).catch((t=>{if("ENOTEMPTY"===t.code)throw new DOMException(...MOD_ERR);throw t})):await Deno.remove(i).catch((()=>{throw new DOMException(...MOD_ERR)})):await Deno.remove(i)}}export default t=>new FolderHandle(join(Deno.cwd(),t));

View File

@ -1 +0,0 @@
import{errors}from"../util.js";import config from"../config.js";const{WritableStream:WritableStream,TransformStream:TransformStream,DOMException:DOMException,Blob:Blob}=config,{GONE:GONE}=errors,isOldSafari=/constructor/i.test(window.HTMLElement);export class FileHandle{constructor(e="unkown"){this.name=e,this.kind="file"}async getFile(){throw new DOMException(...GONE)}async isSameEntry(e){return this===e}async createWritable(e={}){const t=await(navigator.serviceWorker?.getRegistration()),r=document.createElement("a"),s=new TransformStream,a=s.writable;if(r.download=this.name,isOldSafari||!t){let e=[];s.readable.pipeTo(new WritableStream({write(t){e.push(new Blob([t]))},close(){const t=new Blob(e,{type:"application/octet-stream; charset=utf-8"});e=[],r.href=URL.createObjectURL(t),r.click(),setTimeout((()=>URL.revokeObjectURL(r.href)),1e4)}}))}else{const{writable:r,readablePort:a}=new RemoteWritableStream(WritableStream),o=encodeURIComponent(this.name).replace(/['()]/g,escape).replace(/\*/g,"%2A"),n={"content-disposition":"attachment; filename*=UTF-8''"+o,"content-type":"application/octet-stream; charset=utf-8",...e.size?{"content-length":e.size}:{}},i=setTimeout((()=>t.active.postMessage(0)),1e4);s.readable.pipeThrough(new TransformStream({transform(e,t){if(e instanceof Uint8Array)return t.enqueue(e);const r=new Response(e).body.getReader(),s=e=>r.read().then((e=>e.done?0:s(t.enqueue(e.value))));return s()}})).pipeTo(r).finally((()=>{clearInterval(i)})),t.active.postMessage({url:t.scope+o,headers:n,readablePort:a},[a]);const c=document.createElement("iframe");c.hidden=!0,c.src=t.scope+o,document.body.appendChild(c)}return a.getWriter()}}const WRITE=0,PULL=0,ERROR=1,ABORT=1,CLOSE=2;class MessagePortSink{constructor(e){e.onmessage=e=>this._onMessage(e.data),this._port=e,this._resetReady()}start(e){return this._controller=e,this._readyPromise}write(e){const t={type:0,chunk:e};return this._port.postMessage(t,[e.buffer]),this._resetReady(),this._readyPromise}close(){this._port.postMessage({type:2}),this._port.close()}abort(e){this._port.postMessage({type:1,reason:e}),this._port.close()}_onMessage(e){0===e.type&&this._resolveReady(),1===e.type&&this._onError(e.reason)}_onError(e){this._controller.error(e),this._rejectReady(e),this._port.close()}_resetReady(){this._readyPromise=new Promise(((e,t)=>{this._readyResolve=e,this._readyReject=t})),this._readyPending=!0}_resolveReady(){this._readyResolve(),this._readyPending=!1}_rejectReady(e){this._readyPending||this._resetReady(),this._readyPromise.catch((()=>{})),this._readyReject(e),this._readyPending=!1}}class RemoteWritableStream{constructor(e){const t=new MessageChannel;this.readablePort=t.port1,this.writable=new e(new MessagePortSink(t.port2))}}

View File

@ -1 +0,0 @@
import{errors}from"../util.js";const{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX,ABORT:ABORT}=errors;function setupTxErrorHandler(e,t){e.onerror=()=>t(e.error),e.onabort=()=>t(e.error||new DOMException(...ABORT))}class Sink{constructor(e,t,i,s){this.db=e,this.id=t,this.size=i,this.position=0,this.file=s}write(e){if("object"==typeof e)if("write"===e.type){if(Number.isInteger(e.position)&&e.position>=0&&(this.size<e.position&&(this.file=new File([this.file,new ArrayBuffer(e.position-this.size)],this.file.name,this.file)),this.position=e.position),!("data"in e))throw new DOMException(...SYNTAX("write requires a data argument"));e=e.data}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(this.size<e.position)throw new DOMException(...INVALID);return void(this.position=e.position)}throw new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===e.type){if(Number.isInteger(e.size)&&e.size>=0){let t=this.file;return t=e.size<this.size?new File([t.slice(0,e.size)],t.name,t):new File([t,new Uint8Array(e.size-this.size)],t.name,t),this.size=t.size,this.position>t.size&&(this.position=t.size),void(this.file=t)}throw new DOMException(...SYNTAX("truncate requires a size argument"))}}e=new Blob([e]);let t=this.file;const i=t.slice(0,this.position),s=t.slice(this.position+e.size);let n=this.position-i.size;n<0&&(n=0),t=new File([i,new Uint8Array(n),e,s],t.name),this.size=t.size,this.position+=e.size,this.file=t}close(){return new Promise(((e,t)=>{const[i,s]=store(this.db);s.get(this.id).onsuccess=e=>{e.target.result?s.put(this.file,this.id):t(new DOMException(...GONE))},i.oncomplete=()=>e(),i.onerror=t,i.onabort=t}))}}class FileHandle{constructor(e,t,i){this._db=e,this._id=t,this.name=i,this.kind="file",this.readable=!0,this.writable=!0}async isSameEntry(e){return this._id===e._id}async getFile(){const e=await new Promise(((e,t)=>{const i=store(this._db)[1].get(this._id);i.onsuccess=t=>e(t.target.result),i.onerror=e=>t(e.target.error)}));if(!e)throw new DOMException(...GONE);return e}async createWritable(e){let t=await this.getFile();return t=e.keepExistingData?t:new File([],this.name),new Sink(this._db,this._id,t.size,t)}}function store(e){const t=e.transaction("entries","readwrite",{durability:"relaxed"});return[t,t.objectStore("entries")]}function rimraf(e,t,i=!0){const{source:s,result:n}=e.target;for(const[e,r]of Object.values(t||n))r?s.delete(e):i?(s.get(e).onsuccess=rimraf,s.delete(e)):s.get(e).onsuccess=t=>{0!==Object.keys(t.target.result).length?t.target.transaction.abort():s.delete(e)}}class FolderHandle{constructor(e,t,i){this._db=e,this._id=t,this.kind="directory",this.name=i,this.readable=!0,this.writable=!0}async*entries(){const e=store(this._db)[1].get(this._id);await new Promise(((t,i)=>{e.onsuccess=()=>t(),e.onerror=()=>i(e.error)}));const t=e.result;if(!t)throw new DOMException(...GONE);for(const[e,[i,s]]of Object.entries(t))yield[e,s?new FileHandle(this._db,i,e):new FolderHandle(this._db,i,e)]}isSameEntry(e){return this._id===e._id}getDirectoryHandle(e,t){return new Promise(((i,s)=>{const n=store(this._db)[1],r=n.get(this._id);r.onsuccess=()=>{const o=r.result,c=o[e];c?c[1]?s(new DOMException(...MISMATCH)):i(new FolderHandle(this._db,c[0],e)):t.create?n.add({}).onsuccess=t=>{const s=t.target.result;o[e]=[s,!1],n.put(o,this._id).onsuccess=()=>i(new FolderHandle(this._db,s,e))}:s(new DOMException(...GONE))}}))}getFileHandle(e,t){return new Promise(((i,s)=>{const n=store(this._db)[1],r=n.get(this._id);r.onsuccess=()=>{const o=r.result,c=o[e];if(c&&c[1]&&i(new FileHandle(this._db,c[0],e)),c&&!c[1]&&s(new DOMException(...MISMATCH)),c||t.create||s(new DOMException(...GONE)),!c&&t.create){const t=n.put(new File([],e));t.onsuccess=()=>{const s=t.result;o[e]=[s,!0];n.put(o,this._id).onsuccess=()=>{i(new FileHandle(this._db,s,e))}}}}}))}async removeEntry(e,t){return new Promise(((i,s)=>{const[n,r]=store(this._db),o=r.get(this._id);o.onsuccess=i=>{const n=o.result,c={_:n[e]};if(!c._)return s(new DOMException(...GONE));delete n[e],r.put(n,this._id),rimraf(i,c,!!t.recursive)},n.oncomplete=i,n.onerror=s,n.onabort=()=>{s(new DOMException(...MOD_ERR))}}))}}export default(e={persistent:!1})=>new Promise((e=>{const t=indexedDB.open("fileSystem");t.onupgradeneeded=()=>{const e=t.result;e.createObjectStore("entries",{autoIncrement:!0}).transaction.oncomplete=t=>{e.transaction("entries","readwrite").objectStore("entries").add({})}},t.onsuccess=()=>{e(new FolderHandle(t.result,1,""))}}));

View File

@ -1 +0,0 @@
import{errors}from"../util.js";const{GONE:GONE,MISMATCH:MISMATCH,SYNTAX:SYNTAX,DISALLOWED:DISALLOWED}=errors;export class FileHandle{constructor(e,t){this.name=e.name,this.kind="file",this._deleted=!1,this._root=t,this._entry=e,this.writable=!1,this.readable=!0}async getFile(){const e=await fetch(`https://cdn.jsdelivr.net/${this._root}/${this.name}`),t=await e.blob();return new File([t],this.name,{type:t.type,lastModified:this._entry.time})}async createWritable(){throw new DOMException(...DISALLOWED)}async isSameEntry(e){return this===e}}function toDic(e,t){const n={};for(const i of e)i.time=+new Date(i.time),"file"===i.type?n[i.name]=new FileHandle(i,t):n[i.name]=new FolderHandle(i.files,`${t}/${i.name}`,i.name);return n}export class FolderHandle{constructor(e,t,n=""){this.name=n,this.kind="directory",this._deleted=!1,this._entries=toDic(e,t),this.writable=!1,this.readable=!0}async*entries(){yield*Object.entries(this._entries)}async isSameEntry(e){return this===e}async getDirectoryHandle(e,t){if(this._deleted)throw new DOMException(...GONE);const n=this._entries[e];if(n){if(n instanceof FileHandle)throw new DOMException(...MISMATCH);return n}throw t.create?new DOMException(...DISALLOWED):new DOMException(...GONE)}async getFileHandle(e,t){const n=this._entries[e],i=n instanceof FileHandle;if(n&&i)return n;if(n&&!i)throw new DOMException(...MISMATCH);if(!n&&!t.create)throw new DOMException(...GONE);if(!n&&t.create)throw new DOMException(...DISALLOWED)}async removeEntry(e,t){throw new DOMException(...DISALLOWED)}}export default async e=>{const t=await fetch(`https://data.jsdelivr.com/v1/package/${e}`),{files:n}=await t.json();return new FolderHandle(n,e)};

View File

@ -1 +0,0 @@
import{errors}from"../util.js";import config from"../config.js";const{File:File,Blob:Blob,DOMException:DOMException}=config,{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX,SECURITY:SECURITY,DISALLOWED:DISALLOWED}=errors;export class Sink{constructor(e,i){this.fileHandle=e,this.file=i,this.size=i.size,this.position=0}write(e){let i=this.file;if("object"==typeof e)if("write"===e.type){if(Number.isInteger(e.position)&&e.position>=0&&(this.position=e.position,this.size<e.position&&(this.file=new File([this.file,new ArrayBuffer(e.position-this.size)],this.file.name,this.file))),!("data"in e))throw new DOMException(...SYNTAX("write requires a data argument"));e=e.data}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(this.size<e.position)throw new DOMException(...INVALID);return void(this.position=e.position)}throw new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===e.type){if(Number.isInteger(e.size)&&e.size>=0)return i=e.size<this.size?new File([i.slice(0,e.size)],i.name,i):new File([i,new Uint8Array(e.size-this.size)],i.name),this.size=i.size,this.position>i.size&&(this.position=i.size),void(this.file=i);throw new DOMException(...SYNTAX("truncate requires a size argument"))}}e=new Blob([e]);let t=this.file;const s=t.slice(0,this.position),n=t.slice(this.position+e.size);let o=this.position-s.size;o<0&&(o=0),t=new File([s,new Uint8Array(o),e,n],t.name),this.size=t.size,this.position+=e.size,this.file=t}close(){if(this.fileHandle._deleted)throw new DOMException(...GONE);this.fileHandle._file=this.file,this.file=this.position=this.size=null,this.fileHandle.onclose&&this.fileHandle.onclose(this.fileHandle)}}export class FileHandle{constructor(e="",i=new File([],e),t=!0){this._file=i,this.name=e,this.kind="file",this._deleted=!1,this.writable=t,this.readable=!0}async getFile(){if(this._deleted)throw new DOMException(...GONE);return this._file}async createWritable(e){if(!this.writable)throw new DOMException(...DISALLOWED);if(this._deleted)throw new DOMException(...GONE);const i=e.keepExistingData?await this.getFile():new File([],this.name);return new Sink(this,i)}async isSameEntry(e){return this===e}async _destroy(){this._deleted=!0,this._file=null}}export class FolderHandle{constructor(e,i=!0){this.name=e,this.kind="directory",this._deleted=!1,this._entries={},this.writable=i,this.readable=!0}async*entries(){if(this._deleted)throw new DOMException(...GONE);yield*Object.entries(this._entries)}async isSameEntry(e){return this===e}async getDirectoryHandle(e,i){if(this._deleted)throw new DOMException(...GONE);const t=this._entries[e];if(t){if(t instanceof FileHandle)throw new DOMException(...MISMATCH);return t}if(i.create)return this._entries[e]=new FolderHandle(e);throw new DOMException(...GONE)}async getFileHandle(e,i){const t=this._entries[e],s=t instanceof FileHandle;if(t&&s)return t;if(t&&!s)throw new DOMException(...MISMATCH);if(!t&&!i.create)throw new DOMException(...GONE);return!t&&i.create?this._entries[e]=new FileHandle(e):void 0}async removeEntry(e,i){const t=this._entries[e];if(!t)throw new DOMException(...GONE);await t._destroy(i.recursive),delete this._entries[e]}async _destroy(e){for(let i of Object.values(this._entries)){if(!e)throw new DOMException(...MOD_ERR);await i._destroy(e)}this._entries={},this._deleted=!0}}const fs=new FolderHandle("");export default()=>fs;

View File

@ -1 +0,0 @@
import fs from"node:fs/promises";import{join}from"node:path";import{errors}from"../util.js";import config from"../config.js";const{DOMException:DOMException}=config,{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX}=errors;function isBlob(t){return t&&"object"==typeof t&&"function"==typeof t.constructor&&("function"==typeof t.stream||"function"==typeof t.arrayBuffer)&&/^(Blob|File)$/.test(t[Symbol.toStringTag])}export class Sink{constructor(t,i){this._fileHandle=t,this._size=i,this._position=0}async abort(){await this._fileHandle.close()}async write(t){if("object"==typeof t)if("write"===t.type){if(Number.isInteger(t.position)&&t.position>=0&&(this._position=t.position),!("data"in t))throw await this._fileHandle.close(),new DOMException(...SYNTAX("write requires a data argument"));t=t.data}else{if("seek"===t.type){if(Number.isInteger(t.position)&&t.position>=0){if(this._size<t.position)throw new DOMException(...INVALID);return void(this._position=t.position)}throw await this._fileHandle.close(),new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===t.type){if(Number.isInteger(t.size)&&t.size>=0)return await this._fileHandle.truncate(t.size),this._size=t.size,void(this._position>this._size&&(this._position=this._size));throw await this._fileHandle.close(),new DOMException(...SYNTAX("truncate requires a size argument"))}}if(t instanceof ArrayBuffer)t=new Uint8Array(t);else if("string"==typeof t)t=Buffer.from(t);else if(isBlob(t)){for await(const i of t.stream()){const t=await this._fileHandle.writev([i],this._position);this._position+=t.bytesWritten,this._size+=t.bytesWritten}return}const i=await this._fileHandle.writev([t],this._position);this._position+=i.bytesWritten,this._size+=i.bytesWritten}async close(){await this._fileHandle.close()}}export class FileHandle{constructor(t,i){this._path=t,this.name=i,this.kind="file"}async getFile(){await fs.stat(this._path).catch((t=>{if("ENOENT"===t.code)throw new DOMException(...GONE)}));const{fileFrom:t}=await import("fetch-blob/from.js");return t(this._path)}async isSameEntry(t){return this._path===this._getPath.apply(t)}_getPath(){return this._path}async createWritable(t){const i=await fs.open(this._path,t.keepExistingData?"r+":"w+").catch((t=>{if("ENOENT"===t.code)throw new DOMException(...GONE);throw t})),{size:e}=await i.stat();return new Sink(i,e)}}export class FolderHandle{_path="";constructor(t="",i=""){this.name=i,this.kind="directory",this._path=t}async isSameEntry(t){return this._path===t._path}async*entries(){const t=this._path,i=await fs.readdir(t).catch((t=>{if("ENOENT"===t.code)throw new DOMException(...GONE);throw t}));for(let e of i){const i=join(t,e),o=await fs.lstat(i);o.isFile()?yield[e,new FileHandle(i,e)]:o.isDirectory()&&(yield[e,new FolderHandle(i,e)])}}async getDirectoryHandle(t,i){const e=join(this._path,t),o=await fs.lstat(e).catch((t=>{if("ENOENT"!==t.code)throw t})),s=o?.isDirectory();if(o&&s)return new FolderHandle(e,t);if(o&&!s)throw new DOMException(...MISMATCH);if(!i.create)throw new DOMException(...GONE);return await fs.mkdir(e),new FolderHandle(e,t)}async getFileHandle(t,i){const e=join(this._path,t),o=await fs.lstat(e).catch((t=>{if("ENOENT"!==t.code)throw t})),s=o?.isFile();if(o&&s)return new FileHandle(e,t);if(o&&!s)throw new DOMException(...MISMATCH);if(!i.create)throw new DOMException(...GONE);return await(await fs.open(e,"w")).close(),new FileHandle(e,t)}async queryPermission(){return"granted"}async removeEntry(t,i){const e=join(this._path,t);(await fs.lstat(e).catch((t=>{if("ENOENT"===t.code)throw new DOMException(...GONE);throw t}))).isDirectory()?i.recursive?await fs.rm(e,{recursive:!0}).catch((t=>{if("ENOTEMPTY"===t.code)throw new DOMException(...MOD_ERR);throw t})):await fs.rmdir(e).catch((t=>{if("ENOTEMPTY"===t.code)throw new DOMException(...MOD_ERR);throw t})):await fs.unlink(e)}}export default t=>new FolderHandle(t);

View File

@ -1 +0,0 @@
import{errors}from"../util.js";const{DISALLOWED:DISALLOWED}=errors;class Sink{constructor(e,i){this.writer=e,this.fileEntry=i}async write(e){if("object"==typeof e)if("write"===e.type){if(Number.isInteger(e.position)&&e.position>=0&&(this.writer.seek(e.position),this.writer.position!==e.position&&(await new Promise(((i,t)=>{this.writer.onwriteend=i,this.writer.onerror=t,this.writer.truncate(e.position)})),this.writer.seek(e.position))),!("data"in e))throw new DOMException("Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. write requires a data argument","SyntaxError");e=e.data}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(this.writer.seek(e.position),this.writer.position!==e.position)throw new DOMException("seeking position failed","InvalidStateError");return}throw new DOMException("Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. seek requires a position argument","SyntaxError")}if("truncate"===e.type)return new Promise((i=>{if(!(Number.isInteger(e.size)&&e.size>=0))throw new DOMException("Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. truncate requires a size argument","SyntaxError");this.writer.onwriteend=e=>i(),this.writer.truncate(e.size)}))}await new Promise(((i,t)=>{this.writer.onwriteend=i,this.writer.onerror=t,this.writer.write(new Blob([e]))}))}close(){return new Promise(this.fileEntry.file.bind(this.fileEntry))}}export class FileHandle{constructor(e,i=!0){this.file=e,this.kind="file",this.writable=i,this.readable=!0}get name(){return this.file.name}isSameEntry(e){return this.file.toURL()===e.file.toURL()}getFile(){return new Promise(this.file.file.bind(this.file))}createWritable(e){if(!this.writable)throw new DOMException(...DISALLOWED);return new Promise(((i,t)=>this.file.createWriter((t=>{!1===e.keepExistingData?(t.onwriteend=e=>i(new Sink(t,this.file)),t.truncate(0)):i(new Sink(t,this.file))}),t)))}}export class FolderHandle{constructor(e,i=!0){this.dir=e,this.writable=i,this.readable=!0,this.kind="directory",this.name=e.name}isSameEntry(e){return this.dir.fullPath===e.dir.fullPath}async*entries(){const e=this.dir.createReader(),i=await new Promise(e.readEntries.bind(e));for(const e of i)yield[e.name,e.isFile?new FileHandle(e,this.writable):new FolderHandle(e,this.writable)]}getDirectoryHandle(e,i){return new Promise(((t,r)=>{this.dir.getDirectory(e,i,(e=>{t(new FolderHandle(e))}),r)}))}getFileHandle(e,i){return new Promise(((t,r)=>this.dir.getFile(e,i,(e=>t(new FileHandle(e))),r)))}async removeEntry(e,i){const t=await this.getDirectoryHandle(e,{create:!1}).catch((i=>"TypeMismatchError"===i.name?this.getFileHandle(e,{create:!1}):i));if(t instanceof Error)throw t;return new Promise(((e,r)=>{t instanceof FolderHandle?i.recursive?t.dir.removeRecursively((()=>e()),r):t.dir.remove((()=>e()),r):t.file&&t.file.remove((()=>e()),r)}))}}export default(e={})=>new Promise(((i,t)=>window.webkitRequestFileSystem(e._persistent,0,(e=>i(new FolderHandle(e.root))),t)));

View File

@ -1 +0,0 @@
const config={ReadableStream:globalThis.ReadableStream,WritableStream:globalThis.WritableStream,TransformStream:globalThis.TransformStream,DOMException:globalThis.DOMException,Blob:globalThis.Blob,File:globalThis.File};export default config;

View File

@ -1 +0,0 @@
import showDirectoryPicker from"./showDirectoryPicker.js";import showOpenFilePicker from"./showOpenFilePicker.js";import showSaveFilePicker from"./showSaveFilePicker.js";import getOriginPrivateDirectory from"./getOriginPrivateDirectory.js";import FileSystemWritableFileStream from"./FileSystemWritableFileStream.js";import FileSystemDirectoryHandle from"./FileSystemDirectoryHandle.js";import FileSystemFileHandle from"./FileSystemFileHandle.js";import FileSystemHandle from"./FileSystemHandle.js";export{FileSystemDirectoryHandle,FileSystemFileHandle,FileSystemHandle,FileSystemWritableFileStream,getOriginPrivateDirectory,showDirectoryPicker,showOpenFilePicker,showSaveFilePicker};

View File

@ -1 +0,0 @@
async function getOriginPrivateDirectory(e,t={}){if(!e)return globalThis.navigator?.storage?.getDirectory()||globalThis.getOriginPrivateDirectory();const{FileSystemDirectoryHandle:i}=await import("./FileSystemDirectoryHandle.js"),r=await e;return new i(await(r.default?r.default(t):r(t)))}globalThis.DataTransferItem&&!DataTransferItem.prototype.getAsFileSystemHandle&&(DataTransferItem.prototype.getAsFileSystemHandle=async function(){const e=this.webkitGetAsEntry(),[{FileHandle:t,FolderHandle:i},{FileSystemDirectoryHandle:r},{FileSystemFileHandle:a}]=await Promise.all([import("./adapters/sandbox.js"),import("./FileSystemDirectoryHandle.js"),import("./FileSystemFileHandle.js")]);return e.isFile?new a(new t(e,!1)):new r(new i(e,!1))});export default getOriginPrivateDirectory;

View File

@ -1 +0,0 @@
const native=globalThis.showDirectoryPicker;async function showDirectoryPicker(e={}){if(native&&!e._preferPolyfill)return native(e);const t=document.createElement("input");t.type="file",t.webkitdirectory=!0,t.multiple=!0,t.style.position="fixed",t.style.top="-100000px",t.style.left="-100000px",document.body.appendChild(t);const i=import("./util.js");return await new Promise((e=>{t.addEventListener("change",e),t.click()})),i.then((e=>e.getDirHandlesFromInput(t)))}export default showDirectoryPicker;export{showDirectoryPicker};

View File

@ -1 +0,0 @@
const def={accepts:[]},native=globalThis.showOpenFilePicker;async function showOpenFilePicker(e={}){const t={...def,...e};if(native&&!e._preferPolyfill)return native(t);const i=document.createElement("input");i.type="file",i.multiple=t.multiple,i.accept=(t.accepts||[]).map((e=>[...(e.extensions||[]).map((e=>"."+e)),...e.mimeTypes||[]])).flat().join(","),Object.assign(i.style,{position:"fixed",top:"-100000px",left:"-100000px"}),document.body.appendChild(i);const n=import("./util.js");return await new Promise((e=>{i.addEventListener("change",e,{once:!0}),i.click()})),i.remove(),n.then((e=>e.getFileHandlesFromInput(i)))}export default showOpenFilePicker;export{showOpenFilePicker};

View File

@ -1 +0,0 @@
const native=globalThis.showSaveFilePicker;async function showSaveFilePicker(e={}){if(native&&!e._preferPolyfill)return native(e);e._name&&(console.warn("deprecated _name, spec now have `suggestedName`"),e.suggestedName=e._name);const{FileSystemFileHandle:a}=await import("./FileSystemFileHandle.js"),{FileHandle:i}=await import("./adapters/downloader.js");return new a(new i(e.suggestedName))}export default showSaveFilePicker;export{showSaveFilePicker};

View File

@ -1 +0,0 @@
export const errors={INVALID:["seeking position failed.","InvalidStateError"],GONE:["A requested file or directory could not be found at the time an operation was processed.","NotFoundError"],MISMATCH:["The path supplied exists, but was not an entry of requested type.","TypeMismatchError"],MOD_ERR:["The object can not be modified in this way.","InvalidModificationError"],SYNTAX:e=>[`Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. ${e}`,"SyntaxError"],SECURITY:["It was determined that certain files are unsafe for access within a Web application, or that too many calls are being made on file resources.","SecurityError"],DISALLOWED:["The request is not allowed by the user agent or the platform in the current context.","NotAllowedError"]};export const config={writable:globalThis.WritableStream};export async function fromDataTransfer(e){console.warn("deprecated fromDataTransfer - use `dt.items[0].getAsFileSystemHandle()` instead");const[t,r,a]=await Promise.all([import("./adapters/memory.js"),import("./adapters/sandbox.js"),import("./FileSystemDirectoryHandle.js")]),n=new t.FolderHandle("",!1);return n._entries=e.map((e=>e.isFile?new r.FileHandle(e,!1):new r.FolderHandle(e,!1))),new a.FileSystemDirectoryHandle(n)}export async function getDirHandlesFromInput(e){const{FolderHandle:t,FileHandle:r}=await import("./adapters/memory.js"),{FileSystemDirectoryHandle:a}=await import("./FileSystemDirectoryHandle.js"),n=Array.from(e.files),i=n[0].webkitRelativePath.split("/",1)[0],o=new t(i,!1);return n.forEach((e=>{const a=e.webkitRelativePath.split("/");a.shift();const n=a.pop();a.reduce(((e,r)=>(e._entries[r]||(e._entries[r]=new t(r,!1)),e._entries[r])),o)._entries[n]=new r(e.name,e,!1)})),new a(o)}export async function getFileHandlesFromInput(e){const{FileHandle:t}=await import("./adapters/memory.js"),{FileSystemFileHandle:r}=await import("./FileSystemFileHandle.js");return Array.from(e.files).map((e=>new r(new t(e.name,e,!1))))}

View File

@ -1 +0,0 @@
const WRITE=0,PULL=0,ERROR=1,ABORT=1,CLOSE=2,PING=3;class MessagePortSource{controller;constructor(e){this.port=e,this.port.onmessage=e=>this.onMessage(e.data)}start(e){this.controller=e}pull(){this.port.postMessage({type:0})}cancel(e){this.port.postMessage({type:1,reason:e.message}),this.port.close()}onMessage(e){0===e.type&&this.controller.enqueue(e.chunk),1===e.type&&(this.controller.error(e.reason),this.port.close()),2===e.type&&(this.controller.close(),this.port.close())}}self.addEventListener("install",(()=>{self.skipWaiting()})),self.addEventListener("activate",(e=>{e.waitUntil(self.clients.claim())}));const map=new Map;globalThis.addEventListener("message",(e=>{const t=e.data;t.url&&t.readablePort&&(t.rs=new ReadableStream(new MessagePortSource(e.data.readablePort),new CountQueuingStrategy({highWaterMark:4})),map.set(t.url,t))})),globalThis.addEventListener("fetch",(e=>{const t=e.request.url,s=map.get(t);if(!s)return null;map.delete(t),e.respondWith(new Response(s.rs,{headers:s.headers}))}));

View File

@ -1,28 +0,0 @@
BSD 3-Clause License
Copyright (c) 2023, Gildas Lormeau
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,28 +0,0 @@
# Built scripts of zip.js
**Warning**: These files are not compatible with ES modules, i.e. they cannot be imported with `import`. Instead, import `index.js` in the root folder of the project or one of the files prefixed with `zip-` in the [`/lib`](../lib) folder (e.g. `/lib/zip-no-worker-inflate.js`).
- for production (minified):
| | [`ZipReader`](https://gildas-lormeau.github.io/zip.js/api/classes/ZipReader.html) API | [`ZipWriter`](https://gildas-lormeau.github.io/zip.js/api/classes/ZipWriter.html) API | [`zip.fs`](https://gildas-lormeau.github.io/zip.js/api/classes/FS.html) API | Web Workers | No Web Workers | Usage |
|--------------------------------|-----------------|-----------------|--------------|-------------|----------------|-------------------------------------------------------|
| `zip.min.js` | x | x | | x | | compression/decompression with web workers |
| `zip-no-worker.min.js` | x | x | | | x | compression/decompression without web workers |
| `zip-no-worker-inflate.min.js` | x | | | | x | decompression without web workers |
| `zip-no-worker-deflate.min.js` | | x | | | x | compression without web workers |
| `zip-full.min.js` | x | x | | x | x | compression/decompression with or without web workers |
| `zip-fs.min.js` | x | x | x | x | | compression/decompression with web workers |
| `zip-fs-full.min.js` | x | x | x | x | x | compression/decompression with or without web workers |
- for development/debugging:
| | `zip` API | [`zip.fs`](https://gildas-lormeau.github.io/zip.js/api/classes/FS.html) API | Web Workers | No Web Workers |
|-----------------------|-----------|--------------|-------------|----------------|
| `zip.js` | x | | x | |
| `zip-full.js` | x | | x | x |
| `zip-fs.js` | x | x | x | |
| `zip-fs-full.js` | x | x | x | x |
- `z-worker.js` can be used as a web worker script if the [Content Security Policy](https://developer.mozilla.org/docs/Web/HTTP/CSP) blocks scripts loaded with a `blob:` scheme
- `z-worker-fflate.js` is the web worker script for using [fflate](https://gildas-lormeau.github.io/zip.js/core-api.html#alternative-codec-fflate)
- `z-worker-pako.js` is the web worker script for using [pako](https://gildas-lormeau.github.io/zip.js/core-api.html#alternative-codec-pako)

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,5 +1,182 @@
// biome-ignore lint/correctness/noUndeclaredDependencies: shipped by easymde
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
import "easymde/src/css/easymde.css"; import "easymde/src/css/easymde.css";
import EasyMDE from "easymde"; import easyMde from "easymde";
import { markdownRenderMarkdown } from "#openapi";
window.EasyMDE = EasyMDE; /**
* Create a new easymde based textarea
* @param {HTMLTextAreaElement} textarea to use
**/
window.easymdeFactory = (textarea) => {
const easymde = new easyMde({
element: textarea,
spellChecker: false,
autoDownloadFontAwesome: false,
previewRender: Alpine.debounce(async (plainText, preview) => {
preview.innerHTML = (
await markdownRenderMarkdown({ body: { text: plainText } })
).data;
return null;
}, 300),
forceSync: true, // Avoid validation error on generic create view
toolbar: [
{
name: "heading-smaller",
action: easyMde.toggleHeadingSmaller,
className: "fa fa-header",
title: gettext("Heading"),
},
{
name: "italic",
action: easyMde.toggleItalic,
className: "fa fa-italic",
title: gettext("Italic"),
},
{
name: "bold",
action: easyMde.toggleBold,
className: "fa fa-bold",
title: gettext("Bold"),
},
{
name: "strikethrough",
action: easyMde.toggleStrikethrough,
className: "fa fa-strikethrough",
title: gettext("Strikethrough"),
},
{
name: "underline",
action: function customFunction(editor) {
const cm = editor.codemirror;
cm.replaceSelection(`__${cm.getSelection()}__`);
},
className: "fa fa-underline",
title: gettext("Underline"),
},
{
name: "superscript",
action: function customFunction(editor) {
const cm = editor.codemirror;
cm.replaceSelection(`^${cm.getSelection()}^`);
},
className: "fa fa-superscript",
title: gettext("Superscript"),
},
{
name: "subscript",
action: function customFunction(editor) {
const cm = editor.codemirror;
cm.replaceSelection(`~${cm.getSelection()}~`);
},
className: "fa fa-subscript",
title: gettext("Subscript"),
},
{
name: "code",
action: easyMde.toggleCodeBlock,
className: "fa fa-code",
title: gettext("Code"),
},
"|",
{
name: "quote",
action: easyMde.toggleBlockquote,
className: "fa fa-quote-left",
title: gettext("Quote"),
},
{
name: "unordered-list",
action: easyMde.toggleUnorderedList,
className: "fa fa-list-ul",
title: gettext("Unordered list"),
},
{
name: "ordered-list",
action: easyMde.toggleOrderedList,
className: "fa fa-list-ol",
title: gettext("Ordered list"),
},
"|",
{
name: "link",
action: easyMde.drawLink,
className: "fa fa-link",
title: gettext("Insert link"),
},
{
name: "image",
action: easyMde.drawImage,
className: "fa-regular fa-image",
title: gettext("Insert image"),
},
{
name: "table",
action: easyMde.drawTable,
className: "fa fa-table",
title: gettext("Insert table"),
},
"|",
{
name: "clean-block",
action: easyMde.cleanBlock,
className: "fa fa-eraser fa-clean-block",
title: gettext("Clean block"),
},
"|",
{
name: "preview",
action: easyMde.togglePreview,
className: "fa fa-eye no-disable",
title: gettext("Toggle preview"),
},
{
name: "side-by-side",
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-expand no-mobile",
title: gettext("Toggle fullscreen"),
},
"|",
{
name: "guide",
action: "/page/Aide_sur_la_syntaxe",
className: "fa fa-question-circle",
title: gettext("Markdown guide"),
},
],
});
const submits = textarea.closest("form").querySelectorAll('input[type="submit"]');
const parentDiv = textarea.parentElement;
let submitPressed = false;
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) {
parentDiv.style.boxShadow = "red 0px 0px 1.5px 1px";
} else {
parentDiv.style.boxShadow = "";
}
}
function onSubmitClick(e) {
if (!submitPressed) {
easymde.codemirror.on("change", checkMarkdownInput);
}
submitPressed = true;
checkMarkdownInput(e);
}
for (const submit of submits) {
submit.addEventListener("click", onSubmitClick);
}
};

View File

@ -0,0 +1 @@
require("@fortawesome/fontawesome-free/css/all.css");

View File

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

View File

@ -0,0 +1,277 @@
import cytoscape from "cytoscape";
import cxtmenu from "cytoscape-cxtmenu";
import klay from "cytoscape-klay";
import { familyGetFamilyGraph } from "#openapi";
cytoscape.use(klay);
cytoscape.use(cxtmenu);
async function getGraphData(userId, godfathersDepth, godchildrenDepth) {
const data = (
await familyGetFamilyGraph({
path: {
// biome-ignore lint/style/useNamingConvention: api is snake_case
user_id: userId,
},
query: {
// biome-ignore lint/style/useNamingConvention: api is snake_case
godfathers_depth: godfathersDepth,
// biome-ignore lint/style/useNamingConvention: api is snake_case
godchildren_depth: godchildrenDepth,
},
})
).data;
return [
...data.users.map((user) => {
return { data: user };
}),
...data.relationships.map((rel) => {
return {
data: { source: rel.godfather, target: rel.godchild },
};
}),
];
}
function createGraph(container, data, activeUserId) {
const cy = cytoscape({
boxSelectionEnabled: false,
autounselectify: true,
container,
elements: data,
minZoom: 0.5,
style: [
// the stylesheet for the graph
{
selector: "node",
style: {
label: "data(display_name)",
"background-image": "data(profile_pict)",
width: "100%",
height: "100%",
"background-fit": "cover",
"background-repeat": "no-repeat",
shape: "ellipse",
},
},
{
selector: "edge",
style: {
width: 5,
"line-color": "#ccc",
"target-arrow-color": "#ccc",
"target-arrow-shape": "triangle",
"curve-style": "bezier",
},
},
{
selector: ".traversed",
style: {
"border-width": "5px",
"border-style": "solid",
"border-color": "red",
"target-arrow-color": "red",
"line-color": "red",
},
},
{
selector: ".not-traversed",
style: {
"line-opacity": "0.5",
"background-opacity": "0.5",
"background-image-opacity": "0.5",
},
},
],
layout: {
name: "klay",
nodeDimensionsIncludeLabels: true,
fit: true,
klay: {
addUnnecessaryBendpoints: true,
direction: "DOWN",
nodePlacement: "INTERACTIVE",
layoutHierarchy: true,
},
},
});
const activeUser = cy.getElementById(activeUserId).style("shape", "rectangle");
/* Reset graph */
const resetGraph = () => {
cy.elements((element) => {
if (element.hasClass("traversed")) {
element.removeClass("traversed");
}
if (element.hasClass("not-traversed")) {
element.removeClass("not-traversed");
}
});
};
const onNodeTap = (el) => {
resetGraph();
/* Create path on graph if selected isn't the targeted user */
if (el === activeUser) {
return;
}
cy.elements((element) => {
element.addClass("not-traversed");
});
for (const traversed of cy.elements().aStar({
root: el,
goal: activeUser,
}).path) {
traversed.removeClass("not-traversed");
traversed.addClass("traversed");
}
};
cy.on("tap", "node", (tapped) => {
onNodeTap(tapped.target);
});
cy.zoomingEnabled(false);
/* Add context menu */
cy.cxtmenu({
selector: "node",
commands: [
{
content: '<i class="fa fa-external-link fa-2x"></i>',
select: (el) => {
window.open(el.data().profile_url, "_blank").focus();
},
},
{
content: '<span class="fa fa-mouse-pointer fa-2x"></span>',
select: (el) => {
onNodeTap(el);
},
},
{
content: '<i class="fa fa-eraser fa-2x"></i>',
select: (_) => {
resetGraph();
},
},
],
});
return cy;
}
/**
* @typedef FamilyGraphConfig
* @property {number} activeUser Id of the user to fetch the tree from
* @property {number} depthMin Minimum tree depth for godfathers and godchildren
* @property {number} depthMax Maximum tree depth for godfathers and godchildren
**/
/**
* Create a family graph of an user
* @param {FamilyGraphConfig} config
**/
window.loadFamilyGraph = (config) => {
document.addEventListener("alpine:init", () => {
const defaultDepth = 2;
function getInitialDepth(prop) {
// biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
const value = Number.parseInt(initialUrlParams.get(prop));
if (Number.isNaN(value) || value < config.depthMin || value > config.depthMax) {
return defaultDepth;
}
return value;
}
Alpine.data("graph", () => ({
loading: false,
godfathersDepth: getInitialDepth("godfathersDepth"),
godchildrenDepth: getInitialDepth("godchildrenDepth"),
// biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
reverse: initialUrlParams.get("reverse")?.toLowerCase?.() === "true",
graph: undefined,
graphData: {},
async init() {
const delayedFetch = Alpine.debounce(async () => {
await this.fetchGraphData();
}, 100);
for (const param of ["godfathersDepth", "godchildrenDepth"]) {
this.$watch(param, async (value) => {
if (value < config.depthMin || value > config.depthMax) {
return;
}
// biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
updateQueryString(param, value, History.REPLACE);
await delayedFetch();
});
}
this.$watch("reverse", async (value) => {
// biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
updateQueryString("reverse", value, History.REPLACE);
await this.reverseGraph();
});
this.$watch("graphData", async () => {
this.generateGraph();
if (this.reverse) {
await this.reverseGraph();
}
});
await this.fetchGraphData();
},
screenshot() {
const link = document.createElement("a");
link.href = this.graph.jpg();
link.download = interpolate(
gettext("family_tree.%(extension)s"),
{ extension: "jpg" },
true,
);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
reset() {
this.reverse = false;
this.godfathersDepth = defaultDepth;
this.godchildrenDepth = defaultDepth;
},
async reverseGraph() {
this.graph.elements((el) => {
el.position({ x: -el.position().x, y: -el.position().y });
});
this.graph.center(this.graph.elements());
},
async fetchGraphData() {
this.graphData = await getGraphData(
config.activeUser,
this.godfathersDepth,
this.godchildrenDepth,
);
},
generateGraph() {
this.loading = true;
this.graph = createGraph(
$(this.$refs.graph),
this.graphData,
config.activeUser,
);
this.loading = false;
},
}));
});
};

View File

@ -0,0 +1,101 @@
import { paginated } from "#core:utils/api";
import { HttpReader, ZipWriter } from "@zip.js/zip.js";
import { showSaveFilePicker } from "native-file-system-adapter";
import { picturesFetchPictures } from "#openapi";
/**
* @typedef UserProfile
* @property {number} id
* @property {string} first_name
* @property {string} last_name
* @property {string} nick_name
* @property {string} display_name
* @property {string} profile_url
* @property {string} profile_pict
*/
/**
* @typedef Picture
* @property {number} id
* @property {string} name
* @property {number} size
* @property {string} date
* @property {UserProfile} owner
* @property {string} full_size_url
* @property {string} compressed_url
* @property {string} thumb_url
* @property {string} album
* @property {boolean} is_moderated
* @property {boolean} asked_for_removal
*/
/**
* @typedef PicturePageConfig
* @property {number} userId Id of the user to get the pictures from
**/
/**
* Load user picture page with a nice download bar
* @param {PicturePageConfig} config
**/
window.loadPicturePage = (config) => {
document.addEventListener("alpine:init", () => {
Alpine.data("user_pictures", () => ({
isDownloading: false,
loading: true,
pictures: [],
albums: {},
async init() {
this.pictures = await paginated(picturesFetchPictures, {
// biome-ignore lint/style/useNamingConvention: api is in snake_case
query: { users_identified: [config.userId] },
});
this.albums = this.pictures.reduce((acc, picture) => {
if (!acc[picture.album]) {
acc[picture.album] = [];
}
acc[picture.album].push(picture);
return acc;
}, {});
this.loading = false;
},
async downloadZip() {
this.isDownloading = true;
const bar = this.$refs.progress;
bar.value = 0;
bar.max = this.pictures.length;
const incrementProgressBar = () => {
bar.value++;
};
const fileHandle = await showSaveFilePicker({
_preferPolyfill: false,
suggestedName: interpolate(
gettext("pictures.%(extension)s"),
{ extension: "zip" },
true,
),
types: {},
excludeAcceptAllOption: false,
});
const zipWriter = new ZipWriter(await fileHandle.createWritable());
await Promise.all(
this.pictures.map((p) => {
const imgName = `${p.album}/IMG_${p.date.replaceAll(/[:\-]/g, "_")}${p.name.slice(p.name.lastIndexOf("."))}`;
return zipWriter.add(imgName, new HttpReader(p.full_size_url), {
level: 9,
lastModDate: new Date(p.date),
onstart: incrementProgressBar,
});
}),
);
await zipWriter.close();
this.isDownloading = false;
},
}));
});
};

View File

@ -0,0 +1,81 @@
import type { Client, Options, RequestResult } from "@hey-api/client-fetch";
import { client } from "#openapi";
interface PaginatedResponse<T> {
count: number;
next: string | null;
previous: string | null;
results: T[];
}
interface PaginatedRequest {
query?: {
page?: number;
// biome-ignore lint/style/useNamingConvention: api is in snake_case
page_size?: number;
};
}
type PaginatedEndpoint<T> = <ThrowOnError extends boolean = false>(
options?: Options<PaginatedRequest, ThrowOnError>,
) => RequestResult<PaginatedResponse<T>, unknown, ThrowOnError>;
// TODO : If one day a test workflow is made for JS in this project
// please test this function. A all cost.
export const paginated = async <T>(
endpoint: PaginatedEndpoint<T>,
options?: PaginatedRequest,
) => {
const maxPerPage = 199;
options.query.page_size = maxPerPage;
options.query.page = 1;
const firstPage = (await endpoint(options)).data;
const results = firstPage.results;
const nbElements = firstPage.count;
const nbPages = Math.ceil(nbElements / maxPerPage);
if (nbPages > 1) {
const promises: Promise<T[]>[] = [];
for (let i = 2; i <= nbPages; i++) {
const nextPage = structuredClone(options);
nextPage.query.page = i;
promises.push(endpoint(nextPage).then((res) => res.data.results));
}
results.push(...(await Promise.all(promises)).flat());
}
return results;
};
interface Request {
client?: Client;
}
interface InterceptorOptions {
url: string;
}
type GenericEndpoint = <ThrowOnError extends boolean = false>(
options?: Options<Request, ThrowOnError>,
) => RequestResult<unknown, unknown, ThrowOnError>;
/**
* Return the endpoint url of the endpoint
**/
export const makeUrl = async (endpoint: GenericEndpoint) => {
let url = "";
const interceptor = (_request: undefined, options: InterceptorOptions) => {
url = options.url;
throw new Error("We don't want to send the request");
};
client.interceptors.request.use(interceptor);
try {
await endpoint({ client: client });
} catch (_error) {
/* do nothing */
}
client.interceptors.request.eject(interceptor);
return url;
};

View File

@ -18,14 +18,13 @@
{# Thile file is quite heavy (around 250kb), so declaring it in a block allows easy removal #} {# Thile file is quite heavy (around 250kb), so declaring it in a block allows easy removal #}
<link rel="stylesheet" href="{{ static('webpack/jquery-index.css') }}"> <link rel="stylesheet" href="{{ static('webpack/jquery-index.css') }}">
{% endblock %} {% endblock %}
<link rel="preload" as="style" href="{{ static('vendored/font-awesome/css/font-awesome.min.css') }}" onload="this.onload=null;this.rel='stylesheet'"> <link rel="preload" as="style" href="{{ static('webpack/fontawesome-index.css') }}" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{{ static('vendored/font-awesome/css/font-awesome.min.css') }}"></noscript> <noscript><link rel="stylesheet" href="{{ static('webpack/fontawesome-index.css') }}"></noscript>
<script defer href="{{ static('vendored/font-awesome/js/fontawesome.min.js') }}"></script>
<script src="{{ static('webpack/alpine-index.js') }}" defer></script> <script src="{{ static('webpack/alpine-index.js') }}" defer></script>
<!-- Jquery declared here to be accessible in every django widgets --> <!-- Jquery declared here to be accessible in every django widgets -->
<script src="{{ static('webpack/jquery-index.js') }}"></script> <script src="{{ static('webpack/jquery-index.js') }}"></script>
<!-- Put here to always have acces to those functions on django widgets --> <!-- Put here to always have access to those functions on django widgets -->
<script src="{{ static('core/js/script.js') }}"></script> <script src="{{ static('core/js/script.js') }}"></script>
<script defer src="{{ static('vendored/select2/select2.min.js') }}"></script> <script defer src="{{ static('vendored/select2/select2.min.js') }}"></script>
<script defer src="{{ static('core/js/sith-select2.js') }}"></script> <script defer src="{{ static('core/js/sith-select2.js') }}"></script>
@ -69,7 +68,16 @@
</form> </form>
<ul class="bars"> <ul class="bars">
{% cache 100 "counters_activity" %} {% cache 100 "counters_activity" %}
{% for bar in Counter.objects.annotate_has_barman(user).filter(type="BAR") %} {# The sith has no periodic tasks manager
and using cron jobs would be way too overkill here.
Thus the barmen timeout is handled in the only place that
is loaded on every page : the header bar.
However, let's be clear : this has nothing to do here.
It's' merely a contrived workaround that should
replaced by a proper task manager as soon as possible. #}
{% set _ = Counter.objects.filter(type="BAR").handle_timeout() %}
{% endcache %}
{% for bar in Counter.objects.annotate_has_barman(user).annotate_is_open().filter(type="BAR") %}
<li> <li>
{# If the user is a barman, we redirect him directly to the barman page {# If the user is a barman, we redirect him directly to the barman page
else we redirect him to the activity page #} else we redirect him to the activity page #}
@ -78,9 +86,7 @@
{% else %} {% else %}
<a href="{{ url('counter:activity', counter_id=bar.id) }}"> <a href="{{ url('counter:activity', counter_id=bar.id) }}">
{% endif %} {% endif %}
{% if bar.is_inactive() %} {% if bar.is_open %}
<i class="fa fa-question" style="color: #f39c12"></i>
{% elif bar.is_open %}
<i class="fa fa-check" style="color: #2ecc71"></i> <i class="fa fa-check" style="color: #2ecc71"></i>
{% else %} {% else %}
<i class="fa fa-times" style="color: #eb2f06"></i> <i class="fa fa-times" style="color: #eb2f06"></i>
@ -89,7 +95,6 @@
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
{% endcache %}
</ul> </ul>
</div> </div>
<div class="right"> <div class="right">
@ -113,8 +118,8 @@
></a> ></a>
</div> </div>
<div class="notification"> <div class="notification">
<a href="#" onclick="display_notif()"> <a href="#" onclick="displayNotif()">
<i class="fa fa-bell-o"></i> <i class="fa-regular fa-bell"></i>
{% set notification_count = user.notifications.filter(viewed=False).count() %} {% set notification_count = user.notifications.filter(viewed=False).count() %}
{% if notification_count > 0 %} {% if notification_count > 0 %}

View File

@ -40,11 +40,11 @@
{%- endmacro %} {%- endmacro %}
{% macro fb_quick(news) -%} {% macro fb_quick(news) -%}
<a rel="nofollow" target="#" href="https://www.facebook.com/sharer/sharer.php?u={{ news.get_full_url() }}" class="fb fa fa-facebook-square fa-2x"></a> <a rel="nofollow" target="#" href="https://www.facebook.com/sharer/sharer.php?u={{ news.get_full_url() }}" class="fb fa-brands fa-facebook fa-2x"></a>
{%- endmacro %} {%- endmacro %}
{% macro tweet_quick(news) -%} {% macro tweet_quick(news) -%}
<a rel="nofollow" target="#" href="https://twitter.com/intent/tweet?text={{ news.get_full_url() }}" class="twitter fa fa-twitter-square fa-2x"></a> <a rel="nofollow" target="#" href="https://twitter.com/intent/tweet?text={{ news.get_full_url() }}" class="twitter fa-brands fa-twitter-square fa-2x"></a>
{%- endmacro %} {%- endmacro %}
{% macro user_mini_profile(user) %} {% macro user_mini_profile(user) %}

View File

@ -1,30 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<title>{% trans %}Slideshow{% endtrans %}</title>
<link href="{{ static('com/css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="slideshow">
<div id="slides">
{% for poster in posters %}
<div class="slide {% if loop.first %}center{% else %}right{% endif %}" display_time="{{ poster.display_time }}">
<img src="{{ poster.file.url }}"></img>
</div>
{% endfor %}
</div>
<div id="progress_bullets">
{% for poster in posters %}
<div class="bullet {% if loop.first %}active{% endif %}"></div>
{% endfor %}
</div>
<div id="progress_bar"></div>
</div>
<script src="{{ static('core/js/jquery-3.6.2.min.js') }}"></script>
<script src="{{ static('com/js/slideshow.js') }}"></script>
</body>
</html>

View File

@ -1,6 +1,6 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% macro monthly(obj) %} {% macro monthly(objects) %}
<div> <div>
<table> <table>
<thead> <thead>
@ -11,17 +11,18 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for array in obj %} {% for object in objects %}
{% for dict in array %} {% set link=url(
{% if dict['sum'] != 0 %} 'core:user_account_detail',
{% set link=url('core:user_account_detail', user_id=profile.id, year=dict['date'].year, month=dict['date'].month) %} user_id=profile.id,
year=object['grouped_date'].year,
month=object['grouped_date'].month
) %}
<tr> <tr>
<td><a href="{{ link }}">{{ dict['date'].year }}</a></td> <td><a href="{{ link }}">{{ object["grouped_date"]|date("Y") }}</a></td>
<td><a href="{{ link }}">{{ dict['date']|date("E") }}</a></td> <td><a href="{{ link }}">{{ object["grouped_date"]|date("E") }}</a></td>
<td><a href="{{ link }}">{{ dict['sum'] }} €</a></td> <td><a href="{{ link }}">{{ "%.2f"|format(object["total"]) }} €</a></td>
</tr> </tr>
{% endif %}
{% endfor %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@ -37,19 +38,15 @@
<h3>{% trans %}User account{% endtrans %}</h3> <h3>{% trans %}User account{% endtrans %}</h3>
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p> <p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<div id="drop"> <div id="drop">
{% set bought = customer.buyings.exists() %} {% if buyings_month %}
{% set refilled = customer.refillings.exists() %}
{% if bought or refilled %}
{% if bought %}
<h5>{% trans %}Account purchases{% endtrans %}</h5> <h5>{% trans %}Account purchases{% endtrans %}</h5>
{{ monthly(buyings_month) }} {{ monthly(buyings_month) }}
{% endif %} {% endif %}
{% if refilled %} {% if refilling_month %}
<h5>{% trans %}Reloads{% endtrans %}</h5> <h5>{% trans %}Reloads{% endtrans %}</h5>
{{ monthly(refilling_month) }} {{ monthly(refilling_month) }}
{% endif %} {% endif %}
{% endif %} {% if invoices_month %}
{% if customer.user.invoices.exists() %}
<h5>{% trans %}Eboutic invoices{% endtrans %}</h5> <h5>{% trans %}Eboutic invoices{% endtrans %}</h5>
{{ monthly(invoices_month) }} {{ monthly(invoices_month) }}
{% endif %} {% endif %}
@ -58,7 +55,11 @@
<div> <div>
<ul> <ul>
{% for s in etickets %} {% for s in etickets %}
<li><a href="{{ url('counter:eticket_pdf', selling_id=s.id) }}">{{ s.quantity }} x {{ s.product.eticket }}</a></li> <li>
<a href="{{ url('counter:eticket_pdf', selling_id=s.id) }}">
{{ s.quantity }} x {{ s.product.eticket }}
</a>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@ -5,11 +5,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if customer %}
<h3>{% trans %}User account{% endtrans %}</h3> <h3>{% trans %}User account{% endtrans %}</h3>
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p> <p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p> <p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p>
{% if customer.buyings.exists() %} {% if purchases %}
<h4>{% trans %}Account purchases{% endtrans %}</h4> <h4>{% trans %}Account purchases{% endtrans %}</h4>
<table> <table>
<thead> <thead>
@ -24,25 +23,31 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for i in customer.buyings.order_by('-date').all().filter( {% for purchase in purchases %}
date__year=year, date__month=month) %}
<tr> <tr>
<td>{{ i.date|localtime|date(DATETIME_FORMAT) }} - {{ i.date|localtime|time(DATETIME_FORMAT) }}</td> <td>
<td>{{ i.counter }}</td> {{ purchase.date|localtime|date(DATETIME_FORMAT) }}
<td><a href="{{ i.seller.get_absolute_url() }}">{{ i.seller.get_display_name() }}</a></td> - {{ purchase.date|localtime|time(DATETIME_FORMAT) }}
<td>{{ i.label }}</td> </td>
<td>{{ i.quantity }}</td> <td>{{ purchase.counter }}</td>
<td>{{ i.quantity * i.unit_price }} €</td> <td><a href="{{ purchase.seller.get_absolute_url() }}">{{ purchase.seller.get_display_name() }}</a></td>
<td>{{ i.get_payment_method_display() }}</td> <td>{{ purchase.label }}</td>
{% if i.is_owned_by(user) %} <td>{{ purchase.quantity }}</td>
<td><a href="{{ url('counter:selling_delete', selling_id=i.id) }}">{% trans %}Delete{% endtrans %}</a></td> <td>{{ purchase.quantity * purchase.unit_price }} €</td>
<td>{{ purchase.get_payment_method_display() }}</td>
{% if purchase.is_owned_by(user) %}
<td>
<a href="{{ url('counter:selling_delete', selling_id=purchase.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% if customer.refillings.exists() %} {% if refills %}
<h4>{% trans %}Reloads{% endtrans %}</h4> <h4>{% trans %}Reloads{% endtrans %}</h4>
<table> <table>
<thead> <thead>
@ -55,22 +60,30 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for i in customer.refillings.order_by('-date').filter( date__year=year, date__month=month) %} {% for refill in refills %}
<tr> <tr>
<td>{{ i.date|localtime|date(DATETIME_FORMAT) }} - {{ i.date|localtime|time(DATETIME_FORMAT) }}</td> <td>{{ refill.date|localtime|date(DATETIME_FORMAT) }} - {{ refill.date|localtime|time(DATETIME_FORMAT) }}</td>
<td>{{ i.counter }}</td> <td>{{ refill.counter }}</td>
<td><a href="{{ i.operator.get_absolute_url() }}">{{ i.operator.get_display_name() }}</a></td> <td>
<td>{{ i.amount }} €</td> <a href="{{ refill.operator.get_absolute_url() }}">
<td>{{ i.get_payment_method_display() }}</td> {{ refill.operator.get_display_name() }}
{% if i.is_owned_by(user) %} </a>
<td><a href="{{ url('counter:refilling_delete', refilling_id=i.id) }}">{% trans %}Delete{% endtrans %}</a></td> </td>
<td>{{ refill.amount }} €</td>
<td>{{ refill.get_payment_method_display() }}</td>
{% if refill.is_owned_by(user) %}
<td>
<a href="{{ url('counter:refilling_delete', refilling_id=refill.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% if customer.user.invoices.exists() %} {% if invoices %}
<h4>{% trans %}Eboutic invoices{% endtrans %}</h4> <h4>{% trans %}Eboutic invoices{% endtrans %}</h4>
<table> <table>
<thead> <thead>
@ -81,25 +94,24 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for i in customer.user.invoices.order_by('-date').all().filter( {% for invoice in invoices %}
date__year=year, date__month=month) %}
<tr> <tr>
<td>{{ i.date|localtime|date(DATETIME_FORMAT) }} - {{ i.date|localtime|time(DATETIME_FORMAT) }}</td> <td>
{{ invoice.date|localtime|date(DATETIME_FORMAT) }}
- {{ invoice.date|localtime|time(DATETIME_FORMAT) }}
</td>
<td> <td>
<ul> <ul>
{% for it in i.items.all() %} {% for it in invoice.items.all() %}
<li>{{ it.quantity }} x {{ it.product_name }} - {{ it.product_unit_price }} €</li> <li>{{ it.quantity }} x {{ it.product_name }} - {{ it.product_unit_price }} €</li>
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>
<td>{{ i.get_total() }} €</td> <td>{{ invoice.total }} €</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% else %} <p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p>
<p>{% trans %}User has no account{% endtrans %}</p>
{% endif %}
<p><a href="{{ url('core:user_account', user_id=profile.id) }}">{% trans %}Back{% endtrans %}</a></p>
{% endblock %} {% endblock %}

View File

@ -225,7 +225,7 @@
{% for gift in gifts %} {% for gift in gifts %}
<li>{{ gift }} <li>{{ gift }}
<a href="{{ url('core:user_gift_delete', user_id=profile.id, gift_id=gift.id) }}"> <a href="{{ url('core:user_gift_delete', user_id=profile.id, gift_id=gift.id) }}">
<i class="fa fa-trash"></i> <i class="fa-solid fa-trash-can delete-action"></i>
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

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

Some files were not shown because too many files have changed in this diff Show More