Merge pull request #853 from ae-utbm/taiste

Webpack, Forum style and faster counter operations page
This commit is contained in:
thomas girod 2024-10-02 18:03:00 +02:00 committed by GitHub
commit 819cd257a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
101 changed files with 6611 additions and 1552 deletions

View File

@ -39,9 +39,10 @@ jobs:
git fetch
git reset --hard origin/master
poetry install --with prod --without docs,tests
npm install
poetry run ./manage.py install_xapian
poetry run ./manage.py migrate
poetry run ./manage.py collectstatic --clear --noinput
poetry run ./manage.py collectstatic --clear --clear-generated --noinput
poetry run ./manage.py compilemessages
sudo systemctl restart uwsgi

View File

@ -38,9 +38,10 @@ jobs:
git fetch
git reset --hard origin/taiste
poetry install --with prod --without docs,tests
npm install
poetry run ./manage.py install_xapian
poetry run ./manage.py migrate
poetry run ./manage.py collectstatic --clear --noinput
poetry run ./manage.py collectstatic --clear --clear-generated --noinput
poetry run ./manage.py compilemessages
sudo systemctl restart uwsgi

1
.gitignore vendored
View File

@ -17,6 +17,7 @@ sith/settings_custom.py
sith/search_indexes/
.coverage
coverage_report/
node_modules/
# compiled documentation
site/

15
babel.config.json Normal file
View File

@ -0,0 +1,15 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
}
}
]
]
}

View File

@ -1,5 +1,33 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link, paginate %}
{% from 'core/macros.jinja' import user_profile_link %}
{# This page uses a custom macro instead of the core `paginate_jinja` and `paginate_alpine`
because it works with a somewhat dynamic form,
but was written before Alpine was introduced in the project.
TODO : rewrite the pagination used in this template an Alpine one
#}
{% macro paginate(page_obj, paginator, js_action) %}
{% set js = js_action|default('') %}
{% if page_obj.has_previous() or page_obj.has_next() %}
{% if page_obj.has_previous() %}
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.previous_page_number() }}">{% trans %}Previous{% endtrans %}</a>
{% else %}
<span class="disabled">{% trans %}Previous{% endtrans %}</span>
{% endif %}
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<span class="active">{{ i }} <span class="sr-only">({% trans %}current{% endtrans %})</span></span>
{% else %}
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ i }}">{{ i }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next() %}
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.next_page_number() }}">{% trans %}Next{% endtrans %}</a>
{% else %}
<span class="disabled">{% trans %}Next{% endtrans %}</span>
{% endif %}
{% endif %}
{% endmacro %}
{% block content %}
<h3>{% trans %}Sales{% endtrans %}</h3>

View File

@ -2,7 +2,7 @@
<html lang="fr">
<head>
<title>{% trans %}Slideshow{% endtrans %}</title>
<link href="{{ scss('com/css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
<link href="{{ static('com/css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="slideshow">

View File

@ -4,7 +4,6 @@ Some permissions are global (like `IsInGroup` or `IsRoot`),
and some others are per-object (like `CanView` or `CanEdit`).
Examples:
# restrict all the routes of this controller
# to subscribed users
@api_controller("/foo", permissions=[IsSubscriber])

View File

@ -1,12 +1,13 @@
import random
from datetime import date, timedelta
from datetime import timezone as tz
from decimal import Decimal
from typing import Iterator
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db.models import 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.utils.timezone import make_aware, now
from faker import Faker
@ -22,6 +23,7 @@ from counter.models import (
Refilling,
Selling,
)
from forum.models import Forum, ForumMessage, ForumTopic
from pedagogy.models import UV
from subscription.models import Subscription
@ -97,6 +99,8 @@ class Command(BaseCommand):
self.create_sales(sellers)
self.stdout.write("Creating permanences...")
self.create_permanences(sellers)
self.stdout.write("Filling the forum...")
self.create_forums()
self.stdout.write("Done")
@ -288,7 +292,8 @@ class Command(BaseCommand):
since=Subquery(
Subscription.objects.filter(member__customer=OuterRef("pk"))
.annotate(res=Min("subscription_start"))
.values("res")[:1]
.values("res")
.order_by("res")[:1]
)
)
)
@ -381,3 +386,72 @@ class Command(BaseCommand):
)
)
Permanency.objects.bulk_create(perms)
def create_forums(self):
forumers = random.sample(list(User.objects.all()), 100)
most_actives = random.sample(forumers, 10)
categories = list(Forum.objects.filter(is_category=True))
new_forums = [
Forum(name=self.faker.text(20), parent=random.choice(categories))
for _ in range(15)
]
Forum.objects.bulk_create(new_forums)
forums = list(Forum.objects.filter(is_category=False))
new_topics = [
ForumTopic(
_title=self.faker.text(20),
author=random.choice(most_actives),
forum=random.choice(forums),
)
for _ in range(100)
]
ForumTopic.objects.bulk_create(new_topics)
topics = list(ForumTopic.objects.all())
def get_author():
if random.random() > 0.5:
return random.choice(most_actives)
return random.choice(forumers)
messages = []
for t in topics:
nb_messages = max(1, int(random.normalvariate(mu=90, sigma=50)))
dates = sorted(
[
self.faker.date_time_between("-15y", "-1d", tzinfo=tz.utc)
for _ in range(nb_messages)
],
reverse=True,
)
messages.extend(
[
ForumMessage(
topic=t,
author=get_author(),
date=d,
message="\n\n".join(
self.faker.paragraphs(random.randint(1, 4))
),
)
for d in dates
]
)
ForumMessage.objects.bulk_create(messages)
ForumTopic.objects.update(
_message_number=Subquery(
ForumMessage.objects.filter(topic_id=OuterRef("pk"))
.values("topic_id")
.annotate(res=Count("*"))
.values("res")
),
_last_message_id=Subquery(
ForumMessage.objects.order_by("-date").values("id")[:1]
),
)
for f in Forum.objects.filter(parent__isnull=False):
# this is a N+1 queries, but it's ok,
# since there are quite a few forums
# and trying to do it with a single query
# would result in a big whibbly-woobly hacky queryset
f.set_last_message()
f.set_topic_number()

View File

@ -647,7 +647,9 @@ a:not(.button) {
align-items: center;
img {
max-height: 40px;
height: 40px;
width: 40px;
object-fit: cover;
border-radius: 50%;
}
}
@ -1254,165 +1256,6 @@ textarea {
display: inline;
}
/*------------------------------FORUM----------------------------------*/
#forum {
.button {
background-color: rgb(230, 230, 230);
padding: 10px;
font-weight: bold;
border-radius: 5px;
&:hover {
background-color: rgb(211, 211, 211);
}
}
.topic {
border: solid $primary-neutral-color 1px;
padding: 1px;
margin: 1px;
p {
margin: 1px;
font-size: smaller;
}
a {
color: $black-color;
}
a:hover {
text-decoration: underline;
}
}
.tools {
font-size: x-small;
border: none;
font-weight: bold;
a {
padding: 1px;
}
}
.title {
font-size: small;
font-weight: bold;
padding: 2px;
}
.last_message date {
white-space: nowrap;
}
.last_message span {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
display: block;
}
.forum {
background: $primary-neutral-light-color;
padding: 1px;
margin: 1px;
p {
margin: 1px;
font-size: smaller;
}
a {
color: $black-color;
}
a:hover {
text-decoration: underline;
}
}
.search_bar {
margin: 10px 0;
display: flex;
flex-wrap: wrap;
height: 20px;
align-items: center;
}
.search_check {
margin-left: 10px;
}
.search_bouton {
margin-left: 10px;
}
.category {
margin-top: 5px;
background: $secondary-color;
color: white;
border-radius: 10px 10px 0 0;
.title {
text-transform: uppercase;
}
}
.message {
padding: 1px;
margin: 1px;
background: $secondary-neutral-light-color;
&:nth-child(odd) {
background: $primary-neutral-light-color;
}
.title {
font-size: 100%;
}
&.unread {
background: #e9eea1;
}
}
.msg_author.deleted {
background: #ffcfcf;
}
.msg_content {
&.deleted {
background: #ffefef;
}
display: inline-block;
width: 80%;
vertical-align: top;
}
.msg_author {
display: inline-block;
width: 19%;
text-align: center;
img {
max-width: 70%;
margin: 0 auto;
}
}
.msg_header {
display: inline-block;
width: 100%;
font-size: small;
}
.msg_meta {
font-size: small;
list-style-type: none;
li {
padding: 1px;
margin: 1px;
}
}
.forum_signature {
color: hsl(0, 0%, 75%);
border-top: 1px solid hsl(0, 0%, 75%);
a {
color: hsl(0, 0%, 75%);
&:hover {
text-decoration: underline;
}
}
}
}
/*--------------------------------FOOTER-------------------------------*/
footer {

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

View File

@ -1,330 +0,0 @@
Authors ordered by first contribution
A list of current team members is available at http://jqueryui.com/about
Paul Bakaus <paul.bakaus@gmail.com>
Richard Worth <rdworth@gmail.com>
Yehuda Katz <wycats@gmail.com>
Sean Catchpole <sean@sunsean.com>
John Resig <jeresig@gmail.com>
Tane Piper <piper.tane@gmail.com>
Dmitri Gaskin <dmitrig01@gmail.com>
Klaus Hartl <klaus.hartl@gmail.com>
Stefan Petre <stefan.petre@gmail.com>
Gilles van den Hoven <gilles@webunity.nl>
Micheil Bryan Smith <micheil@brandedcode.com>
Jörn Zaefferer <joern.zaefferer@gmail.com>
Marc Grabanski <m@marcgrabanski.com>
Keith Wood <kbwood@iinet.com.au>
Brandon Aaron <brandon.aaron@gmail.com>
Scott González <scott.gonzalez@gmail.com>
Eduardo Lundgren <eduardolundgren@gmail.com>
Aaron Eisenberger <aaronchi@gmail.com>
Joan Piedra <theneojp@gmail.com>
Bruno Basto <b.basto@gmail.com>
Remy Sharp <remy@leftlogic.com>
Bohdan Ganicky <bohdan.ganicky@gmail.com>
David Bolter <david.bolter@gmail.com>
Chi Cheng <cloudream@gmail.com>
Ca-Phun Ung <pazu2k@gmail.com>
Ariel Flesler <aflesler@gmail.com>
Maggie Wachs <maggie@filamentgroup.com>
Scott Jehl <scottjehl@gmail.com>
Todd Parker <todd@filamentgroup.com>
Andrew Powell <andrew@shellscape.org>
Brant Burnett <btburnett3@gmail.com>
Douglas Neiner <doug@dougneiner.com>
Paul Irish <paul.irish@gmail.com>
Ralph Whitbeck <ralph.whitbeck@gmail.com>
Thibault Duplessis <thibault.duplessis@gmail.com>
Dominique Vincent <dominique.vincent@toitl.com>
Jack Hsu <jack.hsu@gmail.com>
Adam Sontag <ajpiano@ajpiano.com>
Carl Fürstenberg <carl@excito.com>
Kevin Dalman <development@allpro.net>
Alberto Fernández Capel <afcapel@gmail.com>
Jacek Jędrzejewski (http://jacek.jedrzejewski.name)
Ting Kuei <ting@kuei.com>
Samuel Cormier-Iijima <sam@chide.it>
Jon Palmer <jonspalmer@gmail.com>
Ben Hollis <bhollis@amazon.com>
Justin MacCarthy <Justin@Rubystars.biz>
Eyal Kobrigo <kobrigo@hotmail.com>
Tiago Freire <tiago.freire@gmail.com>
Diego Tres <diegotres@gmail.com>
Holger Rüprich <holger@rueprich.de>
Ziling Zhao <zilingzhao@gmail.com>
Mike Alsup <malsup@gmail.com>
Robson Braga Araujo <robsonbraga@gmail.com>
Pierre-Henri Ausseil <ph.ausseil@gmail.com>
Christopher McCulloh <cmcculloh@gmail.com>
Andrew Newcomb <ext.github@preceptsoftware.co.uk>
Lim Chee Aun <cheeaun@gmail.com>
Jorge Barreiro <yortx.barry@gmail.com>
Daniel Steigerwald <daniel@steigerwald.cz>
John Firebaugh <john_firebaugh@bigfix.com>
John Enters <github@darkdark.net>
Andrey Kapitcyn <ru.m157y@gmail.com>
Dmitry Petrov <dpetroff@gmail.com>
Eric Hynds <eric@hynds.net>
Chairat Sunthornwiphat <pipo@sixhead.com>
Josh Varner <josh.varner@gmail.com>
Stéphane Raimbault <stephane.raimbault@gmail.com>
Jay Merrifield <fracmak@gmail.com>
J. Ryan Stinnett <jryans@gmail.com>
Peter Heiberg <peter@heiberg.se>
Alex Dovenmuehle <adovenmuehle@gmail.com>
Jamie Gegerson <git@jamiegegerson.com>
Raymond Schwartz <skeetergraphics@gmail.com>
Phillip Barnes <philbar@gmail.com>
Kyle Wilkinson <kai@wikyd.org>
Khaled AlHourani <me@khaledalhourani.com>
Marian Rudzynski <mr@impaled.org>
Jean-Francois Remy <jeff@melix.org>
Doug Blood <dougblood@gmail.com>
Filippo Cavallarin <filippo.cavallarin@codseq.it>
Heiko Henning <heiko@thehennings.ch>
Aliaksandr Rahalevich <saksmlz@gmail.com>
Mario Visic <mario@mariovisic.com>
Xavi Ramirez <xavi.rmz@gmail.com>
Max Schnur <max.schnur@gmail.com>
Saji Nediyanchath <saji89@gmail.com>
Corey Frang <gnarf37@gmail.com>
Aaron Peterson <aaronp123@yahoo.com>
Ivan Peters <ivan@ivanpeters.com>
Mohamed Cherif Bouchelaghem <cherifbouchelaghem@yahoo.fr>
Marcos Sousa <falecomigo@marcossousa.com>
Michael DellaNoce <mdellanoce@mailtrust.com>
George Marshall <echosx@gmail.com>
Tobias Brunner <tobias@strongswan.org>
Martin Solli <msolli@gmail.com>
David Petersen <public@petersendidit.com>
Dan Heberden <danheberden@gmail.com>
William Kevin Manire <williamkmanire@gmail.com>
Gilmore Davidson <gilmoreorless@gmail.com>
Michael Wu <michaelmwu@gmail.com>
Adam Parod <mystic414@gmail.com>
Guillaume Gautreau <guillaume+github@ghusse.com>
Marcel Toele <EleotleCram@gmail.com>
Dan Streetman <ddstreet@ieee.org>
Matt Hoskins <matt@nipltd.com>
Giovanni Giacobbi <giovanni@giacobbi.net>
Kyle Florence <kyle.florence@gmail.com>
Pavol Hluchý <lopo@losys.sk>
Hans Hillen <hans.hillen@gmail.com>
Mark Johnson <virgofx@live.com>
Trey Hunner <treyhunner@gmail.com>
Shane Whittet <whittet@gmail.com>
Edward A Faulkner <ef@alum.mit.edu>
Adam Baratz <adam@adambaratz.com>
Kato Kazuyoshi <kato.kazuyoshi@gmail.com>
Eike Send <eike.send@gmail.com>
Kris Borchers <kris.borchers@gmail.com>
Eddie Monge <eddie@eddiemonge.com>
Israel Tsadok <itsadok@gmail.com>
Carson McDonald <carson@ioncannon.net>
Jason Davies <jason@jasondavies.com>
Garrison Locke <gplocke@gmail.com>
David Murdoch <david@davidmurdoch.com>
Benjamin Scott Boyle <benjamins.boyle@gmail.com>
Jesse Baird <jebaird@gmail.com>
Jonathan Vingiano <jvingiano@gmail.com>
Dylan Just <dev@ephox.com>
Hiroshi Tomita <tomykaira@gmail.com>
Glenn Goodrich <glenn.goodrich@gmail.com>
Tarafder Ashek-E-Elahi <mail.ashek@gmail.com>
Ryan Neufeld <ryan@neufeldmail.com>
Marc Neuwirth <marc.neuwirth@gmail.com>
Philip Graham <philip.robert.graham@gmail.com>
Benjamin Sterling <benjamin.sterling@kenzomedia.com>
Wesley Walser <waw325@gmail.com>
Kouhei Sutou <kou@clear-code.com>
Karl Kirch <karlkrch@gmail.com>
Chris Kelly <ckdake@ckdake.com>
Jason Oster <jay@kodewerx.org>
Felix Nagel <info@felixnagel.com>
Alexander Polomoshnov <alex.polomoshnov@gmail.com>
David Leal <dgleal@gmail.com>
Igor Milla <igor.fsp.milla@gmail.com>
Dave Methvin <dave.methvin@gmail.com>
Florian Gutmann <f.gutmann@chronimo.com>
Marwan Al Jubeh <marwan.aljubeh@gmail.com>
Milan Broum <midlis@googlemail.com>
Sebastian Sauer <info@dynpages.de>
Gaëtan Muller <m.gaetan89@gmail.com>
Michel Weimerskirch <michel@weimerskirch.net>
William Griffiths <william@ycymro.com>
Stojce Slavkovski <stojce@gmail.com>
David Soms <david.soms@gmail.com>
David De Sloovere <david.desloovere@outlook.com>
Michael P. Jung <michael.jung@terreon.de>
Shannon Pekary <spekary@gmail.com>
Dan Wellman <danwellman@hotmail.com>
Matthew Edward Hutton <meh@corefiling.co.uk>
James Khoury <james@jameskhoury.com>
Rob Loach <robloach@gmail.com>
Alberto Monteiro <betimbrasil@gmail.com>
Alex Rhea <alex.rhea@gmail.com>
Krzysztof Rosiński <rozwell69@gmail.com>
Ryan Olton <oltonr@gmail.com>
Genie <386@mail.com>
Rick Waldron <waldron.rick@gmail.com>
Ian Simpson <spoonlikesham@gmail.com>
Lev Kitsis <spam4lev@gmail.com>
TJ VanToll <tj.vantoll@gmail.com>
Justin Domnitz <jdomnitz@gmail.com>
Douglas Cerna <douglascerna@yahoo.com>
Bert ter Heide <bertjh@hotmail.com>
Jasvir Nagra <jasvir@gmail.com>
Yuriy Khabarov <13real008@gmail.com>
Harri Kilpiö <harri.kilpio@gmail.com>
Lado Lomidze <lado.lomidze@gmail.com>
Amir E. Aharoni <amir.aharoni@mail.huji.ac.il>
Simon Sattes <simon.sattes@gmail.com>
Jo Liss <joliss42@gmail.com>
Guntupalli Karunakar <karunakarg@yahoo.com>
Shahyar Ghobadpour <shahyar@gmail.com>
Lukasz Lipinski <uzza17@gmail.com>
Timo Tijhof <krinklemail@gmail.com>
Jason Moon <jmoon@socialcast.com>
Martin Frost <martinf55@hotmail.com>
Eneko Illarramendi <eneko@illarra.com>
EungJun Yi <semtlenori@gmail.com>
Courtland Allen <courtlandallen@gmail.com>
Viktar Varvanovich <non4eg@gmail.com>
Danny Trunk <dtrunk90@gmail.com>
Pavel Stetina <pavel.stetina@nangu.tv>
Michael Stay <metaweta@gmail.com>
Steven Roussey <sroussey@gmail.com>
Michael Hollis <hollis21@gmail.com>
Lee Rowlands <lee.rowlands@previousnext.com.au>
Timmy Willison <timmywillisn@gmail.com>
Karl Swedberg <kswedberg@gmail.com>
Baoju Yuan <the_guy_1987@hotmail.com>
Maciej Mroziński <maciej.k.mrozinski@gmail.com>
Luis Dalmolin <luis.nh@gmail.com>
Mark Aaron Shirley <maspwr@gmail.com>
Martin Hoch <martin@fidion.de>
Jiayi Yang <tr870829@gmail.com>
Philipp Benjamin Köppchen <xgxtpbk@gws.ms>
Sindre Sorhus <sindresorhus@gmail.com>
Bernhard Sirlinger <bernhard.sirlinger@tele2.de>
Jared A. Scheel <jared@jaredscheel.com>
Rafael Xavier de Souza <rxaviers@gmail.com>
John Chen <zhang.z.chen@intel.com>
Robert Beuligmann <robertbeuligmann@gmail.com>
Dale Kocian <dale.kocian@gmail.com>
Mike Sherov <mike.sherov@gmail.com>
Andrew Couch <andy@couchand.com>
Marc-Andre Lafortune <github@marc-andre.ca>
Nate Eagle <nate.eagle@teamaol.com>
David Souther <davidsouther@gmail.com>
Mathias Stenbom <mathias@stenbom.com>
Sergey Kartashov <ebishkek@yandex.ru>
Avinash R <nashpapa@gmail.com>
Ethan Romba <ethanromba@gmail.com>
Cory Gackenheimer <cory.gack@gmail.com>
Juan Pablo Kaniefsky <jpkaniefsky@gmail.com>
Roman Salnikov <bardt.dz@gmail.com>
Anika Henke <anika@selfthinker.org>
Samuel Bovée <samycookie2000@yahoo.fr>
Fabrício Matté <ult_combo@hotmail.com>
Viktor Kojouharov <vkojouharov@gmail.com>
Pawel Maruszczyk (http://hrabstwo.net)
Pavel Selitskas <p.selitskas@gmail.com>
Bjørn Johansen <post@bjornjohansen.no>
Matthieu Penant <thieum22@hotmail.com>
Dominic Barnes <dominic@dbarnes.info>
David Sullivan <david.sullivan@gmail.com>
Thomas Jaggi <thomas@responsive.ch>
Vahid Sohrabloo <vahid4134@gmail.com>
Travis Carden <travis.carden@gmail.com>
Bruno M. Custódio <bruno@brunomcustodio.com>
Nathanael Silverman <nathanael.silverman@gmail.com>
Christian Wenz <christian@wenz.org>
Steve Urmston <steve@urm.st>
Zaven Muradyan <megalivoithos@gmail.com>
Woody Gilk <shadowhand@deviantart.com>
Zbigniew Motyka <zbigniew.motyka@gmail.com>
Suhail Alkowaileet <xsoh.k7@gmail.com>
Toshi MARUYAMA <marutosijp2@yahoo.co.jp>
David Hansen <hansede@gmail.com>
Brian Grinstead <briangrinstead@gmail.com>
Christian Klammer <christian314159@gmail.com>
Steven Luscher <jquerycla@steveluscher.com>
Gan Eng Chin <engchin.gan@gmail.com>
Gabriel Schulhof <gabriel.schulhof@intel.com>
Alexander Schmitz <arschmitz@gmail.com>
Vilhjálmur Skúlason <vis@dmm.is>
Siebrand Mazeland <siebrand@kitano.nl>
Mohsen Ekhtiari <mohsenekhtiari@yahoo.com>
Pere Orga <gotrunks@gmail.com>
Jasper de Groot <mail@ugomobi.com>
Stephane Deschamps <stephane.deschamps@gmail.com>
Jyoti Deka <dekajp@gmail.com>
Andrei Picus <office.nightcrawler@gmail.com>
Ondrej Novy <novy@ondrej.org>
Jacob McCutcheon <jacob.mccutcheon@gmail.com>
Monika Piotrowicz <monika.piotrowicz@gmail.com>
Imants Horsts <imants.horsts@inbox.lv>
Eric Dahl <eric.c.dahl@gmail.com>
Dave Stein <dave@behance.com>
Dylan Barrell <dylan@barrell.com>
Daniel DeGroff <djdegroff@gmail.com>
Michael Wiencek <mwtuea@gmail.com>
Thomas Meyer <meyertee@gmail.com>
Ruslan Yakhyaev <ruslan@ruslan.io>
Brian J. Dowling <bjd-dev@simplicity.net>
Ben Higgins <ben@extrahop.com>
Yermo Lamers <yml@yml.com>
Patrick Stapleton <github@gdi2290.com>
Trisha Crowley <trisha.crowley@gmail.com>
Usman Akeju <akeju00+github@gmail.com>
Rodrigo Menezes <rod333@gmail.com>
Jacques Perrault <jacques_perrault@us.ibm.com>
Frederik Elvhage <frederik.elvhage@googlemail.com>
Will Holley <willholley@gmail.com>
Uri Gilad <antishok@gmail.com>
Richard Gibson <richard.gibson@gmail.com>
Simen Bekkhus <sbekkhus91@gmail.com>
Chen Eshchar <eshcharc@gmail.com>
Bruno Pérel <brunoperel@gmail.com>
Mohammed Alshehri <m@dralshehri.com>
Lisa Seacat DeLuca <ldeluca@us.ibm.com>
Anne-Gaelle Colom <coloma@westminster.ac.uk>
Adam Foster <slimfoster@gmail.com>
Luke Page <luke.a.page@gmail.com>
Daniel Owens <daniel@matchstickmixup.com>
Michael Orchard <morchard@scottlogic.co.uk>
Marcus Warren <marcus@envoke.com>
Nils Heuermann <nils@world-of-scripts.de>
Marco Ziech <marco@ziech.net>
Patricia Juarez <patrixd@gmail.com>
Ben Mosher <me@benmosher.com>
Ablay Keldibek <atomio.ak@gmail.com>
Thomas Applencourt <thomas.applencourt@irsamc.ups-tlse.fr>
Jiabao Wu <jiabao.foss@gmail.com>
Eric Lee Carraway <github@ericcarraway.com>
Victor Homyakov <vkhomyackov@gmail.com>
Myeongjin Lee <aranet100@gmail.com>
Liran Sharir <lsharir@gmail.com>
Weston Ruter <weston@xwp.co>
Mani Mishra <manimishra902@gmail.com>
Hannah Methvin <hannahmethvin@gmail.com>
Leonardo Balter <leonardo.balter@gmail.com>
Benjamin Albert <benjamin_a5@yahoo.com>
Michał Gołębiowski <m.goleb@gmail.com>
Alyosha Pushak <alyosha.pushak@gmail.com>
Fahad Ahmad <fahadahmad41@hotmail.com>
Matt Brundage <github@mattbrundage.com>
Francesc Baeta <francesc.baeta@gmail.com>
Piotr Baran <piotros@wp.pl>
Mukul Hase <mukulhase@gmail.com>
Konstantin Dinev <kdinev@mail.bw.edu>
Rand Scullard <rand@randscullard.com>
Dan Strohl <dan@wjcg.net>
Maksim Ryzhikov <rv.maksim@gmail.com>
Amine HADDAD <haddad@allegorie.tv>
Amanpreet Singh <apsdehal@gmail.com>
Alexey Balchunas <bleshik@gmail.com>
Peter Kehl <peter.kehl@gmail.com>
Peter Dave Hello <hsu@peterdavehello.org>

View File

@ -1,43 +0,0 @@
Copyright jQuery Foundation and other contributors, https://jquery.org/
This software consists of voluntary contributions made by many
individuals. For exact contribution history, see the revision history
available at https://github.com/jquery/jquery-ui
The following license applies to all parts of this software except as
documented below:
====
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.
====
Copyright and related rights for sample code are waived via CC0. Sample
code is defined as all source code contained within the demos directory.
CC0: http://creativecommons.org/publicdomain/zero/1.0/
====
All files located in the node_modules and external directories are
externally maintained libraries used by this software which have their
own licenses; we recommend you read them, as their terms may differ from
the terms above.

View File

@ -1 +0,0 @@
(e=>{"function"==typeof define&&define.amd?define(["../widgets/datepicker"],e):e(jQuery.datepicker)})(function(e){return e.regional.fr={closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},e.setDefaults(e.regional.fr),e.regional.fr});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,545 +0,0 @@
<!doctype html>
<html lang="us">
<head>
<meta charset="utf-8">
<title>jQuery UI Example Page</title>
<link href="jquery-ui.min.css" rel="stylesheet">
<style>
body{
font-family: "Trebuchet MS", sans-serif;
margin: 50px;
}
.demoHeaders {
margin-top: 2em;
}
#dialog-link {
padding: .4em 1em .4em 20px;
text-decoration: none;
position: relative;
}
#dialog-link span.ui-icon {
margin: 0 5px 0 0;
position: absolute;
left: .2em;
top: 50%;
margin-top: -8px;
}
#icons {
margin: 0;
padding: 0;
}
#icons li {
margin: 2px;
position: relative;
padding: 4px 0;
cursor: pointer;
float: left;
list-style: none;
}
#icons span.ui-icon {
float: left;
margin: 0 4px;
}
.fakewindowcontain .ui-widget-overlay {
position: absolute;
}
select {
width: 200px;
}
</style>
</head>
<body>
<h1>Welcome to jQuery UI!</h1>
<div class="ui-widget">
<p>This page demonstrates the widgets and theme you selected in Download Builder. Please make sure you are using them with a compatible jQuery version.</p>
</div>
<h1>YOUR COMPONENTS:</h1>
<!-- Accordion -->
<h2 class="demoHeaders">Accordion</h2>
<div id="accordion">
<h3>First</h3>
<div>Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet.</div>
<h3>Second</h3>
<div>Phasellus mattis tincidunt nibh.</div>
<h3>Third</h3>
<div>Nam dui erat, auctor a, dignissim quis.</div>
</div>
<!-- Autocomplete -->
<h2 class="demoHeaders">Autocomplete</h2>
<div>
<input id="autocomplete" title="type &quot;a&quot;">
</div>
<!-- Button -->
<h2 class="demoHeaders">Button</h2>
<button id="button">A button element</button>
<button id="button-icon">An icon-only button</button>
<!-- Checkboxradio -->
<h2 class="demoHeaders">Checkboxradio</h2>
<form style="margin-top: 1em;">
<div id="radioset">
<input type="radio" id="radio1" name="radio"><label for="radio1">Choice 1</label>
<input type="radio" id="radio2" name="radio" checked="checked"><label for="radio2">Choice 2</label>
<input type="radio" id="radio3" name="radio"><label for="radio3">Choice 3</label>
</div>
</form>
<!-- Controlgroup -->
<h2 class="demoHeaders">Controlgroup</h2>
<fieldset>
<legend>Rental Car</legend>
<div id="controlgroup">
<select id="car-type">
<option>Compact car</option>
<option>Midsize car</option>
<option>Full size car</option>
<option>SUV</option>
<option>Luxury</option>
<option>Truck</option>
<option>Van</option>
</select>
<label for="transmission-standard">Standard</label>
<input type="radio" name="transmission" id="transmission-standard">
<label for="transmission-automatic">Automatic</label>
<input type="radio" name="transmission" id="transmission-automatic">
<label for="insurance">Insurance</label>
<input type="checkbox" name="insurance" id="insurance">
<label for="horizontal-spinner" class="ui-controlgroup-label"># of cars</label>
<input id="horizontal-spinner" class="ui-spinner-input">
<button>Book Now!</button>
</div>
</fieldset>
<!-- Tabs -->
<h2 class="demoHeaders">Tabs</h2>
<div id="tabs">
<ul>
<li><a href="#tabs-1">First</a></li>
<li><a href="#tabs-2">Second</a></li>
<li><a href="#tabs-3">Third</a></li>
</ul>
<div id="tabs-1">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</div>
<div id="tabs-2">Phasellus mattis tincidunt nibh. Cras orci urna, blandit id, pretium vel, aliquet ornare, felis. Maecenas scelerisque sem non nisl. Fusce sed lorem in enim dictum bibendum.</div>
<div id="tabs-3">Nam dui erat, auctor a, dignissim quis, sollicitudin eu, felis. Pellentesque nisi urna, interdum eget, sagittis et, consequat vestibulum, lacus. Mauris porttitor ullamcorper augue.</div>
</div>
<h2 class="demoHeaders">Dialog</h2>
<p>
<button id="dialog-link" class="ui-button ui-corner-all ui-widget">
<span class="ui-icon ui-icon-newwin"></span>Open Dialog
</button>
</p>
<h2 class="demoHeaders">Overlay and Shadow Classes</h2>
<div style="position: relative; width: 96%; height: 200px; padding:1% 2%; overflow:hidden;" class="fakewindowcontain">
<p>Lorem ipsum dolor sit amet, Nulla nec tortor. Donec id elit quis purus consectetur consequat. </p><p>Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. </p><p>Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. Nullam feugiat cursus lacus.orem ipsum dolor sit amet, consectetur adipiscing elit. Donec libero risus, commodo vitae, pharetra mollis, posuere eu, pede. Nulla nec tortor. Donec id elit quis purus consectetur consequat. </p><p>Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. </p><p>Nullam feugiat cursus lacus.orem ipsum dolor sit amet, consectetur adipiscing elit. Donec libero risus, commodo vitae, pharetra mollis, posuere eu, pede. Nulla nec tortor. Donec id elit quis purus consectetur consequat. Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. </p><p>Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. Nullam feugiat cursus lacus.orem ipsum dolor sit amet, consectetur adipiscing elit. Donec libero risus, commodo vitae, pharetra mollis, posuere eu, pede. Nulla nec tortor. Donec id elit quis purus consectetur consequat. Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. </p>
<!-- ui-dialog -->
<div class="ui-widget-overlay ui-front"></div>
<div style="position: absolute; width: 320px; left: 50px; top: 30px; padding: 1.2em" class="ui-widget ui-front ui-widget-content ui-corner-all ui-widget-shadow">
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</div>
</div>
<!-- ui-dialog -->
<div id="dialog" title="Dialog Title">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</div>
<h2 class="demoHeaders">Framework Icons (content color preview)</h2>
<ul id="icons" class="ui-widget ui-helper-clearfix">
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-n"><span class="ui-icon ui-icon-caret-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-ne"><span class="ui-icon ui-icon-caret-1-ne"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-e"><span class="ui-icon ui-icon-caret-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-se"><span class="ui-icon ui-icon-caret-1-se"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-s"><span class="ui-icon ui-icon-caret-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-sw"><span class="ui-icon ui-icon-caret-1-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-w"><span class="ui-icon ui-icon-caret-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-nw"><span class="ui-icon ui-icon-caret-1-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-2-n-s"><span class="ui-icon ui-icon-caret-2-n-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-2-e-w"><span class="ui-icon ui-icon-caret-2-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-n"><span class="ui-icon ui-icon-triangle-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-ne"><span class="ui-icon ui-icon-triangle-1-ne"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-e"><span class="ui-icon ui-icon-triangle-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-se"><span class="ui-icon ui-icon-triangle-1-se"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-s"><span class="ui-icon ui-icon-triangle-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-sw"><span class="ui-icon ui-icon-triangle-1-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-w"><span class="ui-icon ui-icon-triangle-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-nw"><span class="ui-icon ui-icon-triangle-1-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-2-n-s"><span class="ui-icon ui-icon-triangle-2-n-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-2-e-w"><span class="ui-icon ui-icon-triangle-2-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-n"><span class="ui-icon ui-icon-arrow-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-ne"><span class="ui-icon ui-icon-arrow-1-ne"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-e"><span class="ui-icon ui-icon-arrow-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-se"><span class="ui-icon ui-icon-arrow-1-se"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-s"><span class="ui-icon ui-icon-arrow-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-sw"><span class="ui-icon ui-icon-arrow-1-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-w"><span class="ui-icon ui-icon-arrow-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-nw"><span class="ui-icon ui-icon-arrow-1-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-n-s"><span class="ui-icon ui-icon-arrow-2-n-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-ne-sw"><span class="ui-icon ui-icon-arrow-2-ne-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-e-w"><span class="ui-icon ui-icon-arrow-2-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-se-nw"><span class="ui-icon ui-icon-arrow-2-se-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-n"><span class="ui-icon ui-icon-arrowstop-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-e"><span class="ui-icon ui-icon-arrowstop-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-s"><span class="ui-icon ui-icon-arrowstop-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-w"><span class="ui-icon ui-icon-arrowstop-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-n"><span class="ui-icon ui-icon-arrowthick-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-ne"><span class="ui-icon ui-icon-arrowthick-1-ne"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-e"><span class="ui-icon ui-icon-arrowthick-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-se"><span class="ui-icon ui-icon-arrowthick-1-se"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-s"><span class="ui-icon ui-icon-arrowthick-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-sw"><span class="ui-icon ui-icon-arrowthick-1-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-w"><span class="ui-icon ui-icon-arrowthick-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-nw"><span class="ui-icon ui-icon-arrowthick-1-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-n-s"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-ne-sw"><span class="ui-icon ui-icon-arrowthick-2-ne-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-e-w"><span class="ui-icon ui-icon-arrowthick-2-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-se-nw"><span class="ui-icon ui-icon-arrowthick-2-se-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-n"><span class="ui-icon ui-icon-arrowthickstop-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-e"><span class="ui-icon ui-icon-arrowthickstop-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-s"><span class="ui-icon ui-icon-arrowthickstop-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-w"><span class="ui-icon ui-icon-arrowthickstop-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-w"><span class="ui-icon ui-icon-arrowreturnthick-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-n"><span class="ui-icon ui-icon-arrowreturnthick-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-e"><span class="ui-icon ui-icon-arrowreturnthick-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-s"><span class="ui-icon ui-icon-arrowreturnthick-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-w"><span class="ui-icon ui-icon-arrowreturn-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-n"><span class="ui-icon ui-icon-arrowreturn-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-e"><span class="ui-icon ui-icon-arrowreturn-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-s"><span class="ui-icon ui-icon-arrowreturn-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-w"><span class="ui-icon ui-icon-arrowrefresh-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-n"><span class="ui-icon ui-icon-arrowrefresh-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-e"><span class="ui-icon ui-icon-arrowrefresh-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-s"><span class="ui-icon ui-icon-arrowrefresh-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-4"><span class="ui-icon ui-icon-arrow-4"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-4-diag"><span class="ui-icon ui-icon-arrow-4-diag"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-extlink"><span class="ui-icon ui-icon-extlink"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-newwin"><span class="ui-icon ui-icon-newwin"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-refresh"><span class="ui-icon ui-icon-refresh"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-shuffle"><span class="ui-icon ui-icon-shuffle"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-transfer-e-w"><span class="ui-icon ui-icon-transfer-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-transferthick-e-w"><span class="ui-icon ui-icon-transferthick-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-folder-collapsed"><span class="ui-icon ui-icon-folder-collapsed"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-folder-open"><span class="ui-icon ui-icon-folder-open"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-document"><span class="ui-icon ui-icon-document"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-document-b"><span class="ui-icon ui-icon-document-b"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-note"><span class="ui-icon ui-icon-note"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-mail-closed"><span class="ui-icon ui-icon-mail-closed"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-mail-open"><span class="ui-icon ui-icon-mail-open"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-suitcase"><span class="ui-icon ui-icon-suitcase"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-comment"><span class="ui-icon ui-icon-comment"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-person"><span class="ui-icon ui-icon-person"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-print"><span class="ui-icon ui-icon-print"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-trash"><span class="ui-icon ui-icon-trash"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-locked"><span class="ui-icon ui-icon-locked"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-unlocked"><span class="ui-icon ui-icon-unlocked"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-bookmark"><span class="ui-icon ui-icon-bookmark"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-tag"><span class="ui-icon ui-icon-tag"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-home"><span class="ui-icon ui-icon-home"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-flag"><span class="ui-icon ui-icon-flag"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-calculator"><span class="ui-icon ui-icon-calculator"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-cart"><span class="ui-icon ui-icon-cart"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-pencil"><span class="ui-icon ui-icon-pencil"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-clock"><span class="ui-icon ui-icon-clock"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-disk"><span class="ui-icon ui-icon-disk"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-calendar"><span class="ui-icon ui-icon-calendar"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-zoomin"><span class="ui-icon ui-icon-zoomin"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-zoomout"><span class="ui-icon ui-icon-zoomout"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-search"><span class="ui-icon ui-icon-search"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-wrench"><span class="ui-icon ui-icon-wrench"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-gear"><span class="ui-icon ui-icon-gear"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-heart"><span class="ui-icon ui-icon-heart"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-star"><span class="ui-icon ui-icon-star"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-link"><span class="ui-icon ui-icon-link"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-cancel"><span class="ui-icon ui-icon-cancel"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-plus"><span class="ui-icon ui-icon-plus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-plusthick"><span class="ui-icon ui-icon-plusthick"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-minus"><span class="ui-icon ui-icon-minus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-minusthick"><span class="ui-icon ui-icon-minusthick"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-close"><span class="ui-icon ui-icon-close"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-closethick"><span class="ui-icon ui-icon-closethick"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-key"><span class="ui-icon ui-icon-key"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-lightbulb"><span class="ui-icon ui-icon-lightbulb"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-scissors"><span class="ui-icon ui-icon-scissors"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-clipboard"><span class="ui-icon ui-icon-clipboard"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-copy"><span class="ui-icon ui-icon-copy"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-contact"><span class="ui-icon ui-icon-contact"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-image"><span class="ui-icon ui-icon-image"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-video"><span class="ui-icon ui-icon-video"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-script"><span class="ui-icon ui-icon-script"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-alert"><span class="ui-icon ui-icon-alert"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-info"><span class="ui-icon ui-icon-info"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-notice"><span class="ui-icon ui-icon-notice"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-help"><span class="ui-icon ui-icon-help"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-check"><span class="ui-icon ui-icon-check"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-bullet"><span class="ui-icon ui-icon-bullet"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-radio-off"><span class="ui-icon ui-icon-radio-off"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-radio-on"><span class="ui-icon ui-icon-radio-on"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-pin-w"><span class="ui-icon ui-icon-pin-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-pin-s"><span class="ui-icon ui-icon-pin-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-play"><span class="ui-icon ui-icon-play"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-pause"><span class="ui-icon ui-icon-pause"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-next"><span class="ui-icon ui-icon-seek-next"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-prev"><span class="ui-icon ui-icon-seek-prev"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-end"><span class="ui-icon ui-icon-seek-end"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-first"><span class="ui-icon ui-icon-seek-first"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-stop"><span class="ui-icon ui-icon-stop"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-eject"><span class="ui-icon ui-icon-eject"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-volume-off"><span class="ui-icon ui-icon-volume-off"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-volume-on"><span class="ui-icon ui-icon-volume-on"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-power"><span class="ui-icon ui-icon-power"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-signal-diag"><span class="ui-icon ui-icon-signal-diag"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-signal"><span class="ui-icon ui-icon-signal"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-0"><span class="ui-icon ui-icon-battery-0"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-1"><span class="ui-icon ui-icon-battery-1"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-2"><span class="ui-icon ui-icon-battery-2"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-3"><span class="ui-icon ui-icon-battery-3"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-plus"><span class="ui-icon ui-icon-circle-plus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-minus"><span class="ui-icon ui-icon-circle-minus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-close"><span class="ui-icon ui-icon-circle-close"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-e"><span class="ui-icon ui-icon-circle-triangle-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-s"><span class="ui-icon ui-icon-circle-triangle-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-w"><span class="ui-icon ui-icon-circle-triangle-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-n"><span class="ui-icon ui-icon-circle-triangle-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-e"><span class="ui-icon ui-icon-circle-arrow-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-s"><span class="ui-icon ui-icon-circle-arrow-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-w"><span class="ui-icon ui-icon-circle-arrow-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-n"><span class="ui-icon ui-icon-circle-arrow-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-zoomin"><span class="ui-icon ui-icon-circle-zoomin"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-zoomout"><span class="ui-icon ui-icon-circle-zoomout"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-check"><span class="ui-icon ui-icon-circle-check"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-plus"><span class="ui-icon ui-icon-circlesmall-plus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-minus"><span class="ui-icon ui-icon-circlesmall-minus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-close"><span class="ui-icon ui-icon-circlesmall-close"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-plus"><span class="ui-icon ui-icon-squaresmall-plus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-minus"><span class="ui-icon ui-icon-squaresmall-minus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-close"><span class="ui-icon ui-icon-squaresmall-close"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-dotted-vertical"><span class="ui-icon ui-icon-grip-dotted-vertical"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-dotted-horizontal"><span class="ui-icon ui-icon-grip-dotted-horizontal"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-solid-vertical"><span class="ui-icon ui-icon-grip-solid-vertical"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-solid-horizontal"><span class="ui-icon ui-icon-grip-solid-horizontal"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-gripsmall-diagonal-se"><span class="ui-icon ui-icon-gripsmall-diagonal-se"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-diagonal-se"><span class="ui-icon ui-icon-grip-diagonal-se"></span></li>
</ul>
<!-- Slider -->
<h2 class="demoHeaders">Slider</h2>
<div id="slider"></div>
<!-- Datepicker -->
<h2 class="demoHeaders">Datepicker</h2>
<div id="datepicker"></div>
<!-- Progressbar -->
<h2 class="demoHeaders">Progressbar</h2>
<div id="progressbar"></div>
<!-- Progressbar -->
<h2 class="demoHeaders">Selectmenu</h2>
<select id="selectmenu">
<option>Slower</option>
<option>Slow</option>
<option selected="selected">Medium</option>
<option>Fast</option>
<option>Faster</option>
</select>
<!-- Spinner -->
<h2 class="demoHeaders">Spinner</h2>
<input id="spinner">
<!-- Menu -->
<h2 class="demoHeaders">Menu</h2>
<ul style="width:100px;" id="menu">
<li><div>Item 1</div></li>
<li><div>Item 2</div></li>
<li><div>Item 3</div>
<ul>
<li><div>Item 3-1</div></li>
<li><div>Item 3-2</div></li>
<li><div>Item 3-3</div></li>
<li><div>Item 3-4</div></li>
<li><div>Item 3-5</div></li>
</ul>
</li>
<li><div>Item 4</div></li>
<li><div>Item 5</div></li>
</ul>
<!-- Highlight / Error -->
<h2 class="demoHeaders">Highlight / Error</h2>
<div class="ui-widget">
<div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0 .7em;">
<p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>
<strong>Hey!</strong> Sample ui-state-highlight style.</p>
</div>
</div>
<br>
<div class="ui-widget">
<div class="ui-state-error ui-corner-all" style="padding: 0 .7em;">
<p><span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>
<strong>Alert:</strong> Sample ui-state-error style.</p>
</div>
</div>
<script src="../jquery-3.6.2.min.js"></script>
<script src="jquery-ui.min.js"></script>
<script>
$( "#accordion" ).accordion();
var availableTags = [
"ActionScript",
"AppleScript",
"Asp",
"BASIC",
"C",
"C++",
"Clojure",
"COBOL",
"ColdFusion",
"Erlang",
"Fortran",
"Groovy",
"Haskell",
"Java",
"JavaScript",
"Lisp",
"Perl",
"PHP",
"Python",
"Ruby",
"Scala",
"Scheme"
];
$( "#autocomplete" ).autocomplete({
source: availableTags
});
$( "#button" ).button();
$( "#button-icon" ).button({
icon: "ui-icon-gear",
showLabel: false
});
$( "#radioset" ).buttonset();
$( "#controlgroup" ).controlgroup();
$( "#tabs" ).tabs();
$( "#dialog" ).dialog({
autoOpen: false,
width: 400,
buttons: [
{
text: "Ok",
click: function() {
$( this ).dialog( "close" );
}
},
{
text: "Cancel",
click: function() {
$( this ).dialog( "close" );
}
}
]
});
// Link to open the dialog
$( "#dialog-link" ).click(function( event ) {
$( "#dialog" ).dialog( "open" );
event.preventDefault();
});
$( "#datepicker" ).datepicker({
inline: true
});
$( "#slider" ).slider({
range: true,
values: [ 17, 67 ]
});
$( "#progressbar" ).progressbar({
value: 20
});
$( "#spinner" ).spinner();
$( "#menu" ).menu();
$( "#selectmenu" ).selectmenu();
// Hover states on the static widgets
$( "#dialog-link, #icons li" ).hover(
function() {
$( this ).addClass( "ui-state-hover" );
},
function() {
$( this ).removeClass( "ui-state-hover" );
}
);
</script>
</body>
</html>

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

View File

@ -1,74 +0,0 @@
{
"name": "jquery-ui",
"title": "jQuery UI",
"description": "A curated set of user interface interactions, effects, widgets, and themes built on top of the jQuery JavaScript Library.",
"version": "1.12.0",
"homepage": "http://jqueryui.com",
"author": {
"name": "jQuery Foundation and other contributors",
"url": "https://github.com/jquery/jquery-ui/blob/1.12.0/AUTHORS.txt"
},
"main": "ui/widget.js",
"maintainers": [
{
"name": "Scott González",
"email": "scott.gonzalez@gmail.com",
"url": "http://scottgonzalez.com"
},
{
"name": "Jörn Zaefferer",
"email": "joern.zaefferer@gmail.com",
"url": "http://bassistance.de"
},
{
"name": "Mike Sherov",
"email": "mike.sherov@gmail.com",
"url": "http://mike.sherov.com"
},
{
"name": "TJ VanToll",
"email": "tj.vantoll@gmail.com",
"url": "http://tjvantoll.com"
},
{
"name": "Felix Nagel",
"email": "info@felixnagel.com",
"url": "http://www.felixnagel.com"
},
{
"name": "Alex Schmitz",
"email": "arschmitz@gmail.com",
"url": "https://github.com/arschmitz"
}
],
"repository": {
"type": "git",
"url": "git://github.com/jquery/jquery-ui.git"
},
"bugs": "https://bugs.jqueryui.com/",
"license": "MIT",
"scripts": {
"test": "grunt"
},
"dependencies": {},
"devDependencies": {
"commitplease": "2.3.0",
"grunt": "0.4.5",
"grunt-bowercopy": "1.2.4",
"grunt-cli": "0.1.13",
"grunt-compare-size": "0.4.0",
"grunt-contrib-concat": "0.5.1",
"grunt-contrib-csslint": "0.5.0",
"grunt-contrib-jshint": "0.12.0",
"grunt-contrib-qunit": "1.0.1",
"grunt-contrib-requirejs": "0.4.4",
"grunt-contrib-uglify": "0.11.1",
"grunt-git-authors": "3.1.0",
"grunt-html": "6.0.0",
"grunt-jscs": "2.1.0",
"load-grunt-tasks": "3.4.0",
"rimraf": "2.5.1",
"testswarm": "1.1.0"
},
"keywords": []
}

View File

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

View File

@ -0,0 +1,5 @@
import "codemirror/lib/codemirror.css";
import "easymde/src/css/easymde.css";
import EasyMDE from "easymde";
window.EasyMDE = EasyMDE;

25
core/static/webpack/jquery-index.js vendored Normal file
View File

@ -0,0 +1,25 @@
import $ from "jquery";
import "jquery.shorten/src/jquery.shorten.min.js";
// We ship jquery-ui with jquery because when standalone with webpack
// JQuery is also included in the jquery-ui package. We do gain space by doing this
// We require jquery-ui components manually and not in a loop
// Otherwise it increases the output files by a x2 factor !
require("jquery-ui/ui/widgets/accordion.js");
require("jquery-ui/ui/widgets/autocomplete.js");
require("jquery-ui/ui/widgets/button.js");
require("jquery-ui/ui/widgets/dialog.js");
require("jquery-ui/ui/widgets/tabs.js");
require("jquery-ui/themes/base/all.css");
/**
* Simple wrapper to solve shorten not being able on legacy pages
* @param {string} selector to be passed to jQuery
* @param {Object} options object to pass to the shorten function
**/
export function shorten(selector, options) {
$(selector).shorten(options);
}
window.shorten = shorten;

View File

@ -7,33 +7,33 @@
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link rel="stylesheet" href="{{ static('core/base.css') }}">
<link rel="stylesheet" href="{{ static('ajax_select/css/ajax_select.css') }}">
<link rel="stylesheet" href="{{ scss('core/style.scss') }}">
<link rel="stylesheet" href="{{ scss('core/markdown.scss') }}">
<link rel="stylesheet" href="{{ scss('core/header.scss') }}">
<link rel="stylesheet" href="{{ scss('core/navbar.scss') }}">
<link rel="stylesheet" href="{{ static('core/style.scss') }}">
<link rel="stylesheet" href="{{ static('core/markdown.scss') }}">
<link rel="stylesheet" href="{{ static('core/header.scss') }}">
<link rel="stylesheet" href="{{ static('core/navbar.scss') }}">
<link rel="stylesheet" href="{{ static('core/pagination.scss') }}">
<link rel="stylesheet" href="{{ static('vendored/select2/select2.min.css') }}">
{% block jquery_css %}
{# Thile file is quite heavy (around 250kb), so declaring it in a block allows easy removal #}
<link rel="stylesheet" href="{{ static('vendored/jquery/ui/jquery-ui.min.css') }}">
<link rel="stylesheet" href="{{ static('webpack/jquery-index.css') }}">
{% endblock %}
<link rel="preload" as="style" href="{{ static('vendored/font-awesome/css/font-awesome.min.css') }}" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{{ static('vendored/font-awesome/css/font-awesome.min.css') }}"></noscript>
<script defer href="{{ static('vendored/font-awesome/js/fontawesome.min.js') }}"></script>
<script src="{{ static('webpack/alpine-index.js') }}" defer></script>
<!-- Jquery declared here to be accessible in every django widgets -->
<script src="{{ static('vendored/jquery/jquery-3.6.2.min.js') }}"></script>
<script src="{{ static('webpack/jquery-index.js') }}"></script>
<!-- Put here to always have acces to those functions on django widgets -->
<script src="{{ static('core/js/script.js') }}"></script>
<script defer src="{{ static('vendored/select2/select2.min.js') }}"></script>
<script defer src="{{ static('core/js/sith-select2.js') }}"></script>
{% block additional_css %}{% endblock %}
{% block additional_js %}{% endblock %}
{# Alpine JS must be loaded after scripts that use it. #}
<script src="{{ static('vendored/alpine/alpinejs.min.js') }}" defer></script>
{% endblock %}
</head>
@ -298,7 +298,6 @@
{% endif %}
{% block script %}
<script src="{{ static('vendored/jquery/ui/jquery-ui.min.js') }}"></script>
<script src="{{ static('ajax_select/js/ajax_select.js') }}"></script>
<script src="{{ url('javascript-catalog') }}"></script>
<script>

View File

@ -1,7 +1,7 @@
{% extends "core/base.jinja" %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('user/login.scss') }}">
<link rel="stylesheet" href="{{ static('user/login.scss') }}">
{%- endblock -%}
{% block title %}

View File

@ -156,27 +156,47 @@
</nav>
{% endmacro %}
{% macro paginate(page_obj, paginator, js_action) %}
{% set js = js_action|default('') %}
{% if page_obj.has_previous() or page_obj.has_next() %}
{% if page_obj.has_previous() %}
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.previous_page_number() }}">{% trans %}Previous{% endtrans %}</a>
{% macro paginate_jinja(current_page, paginator) %}
{# Add pagination buttons for pages without Alpine.
This must be coupled with a view that handles pagination
with the Django Paginator object.
Parameters:
current_page (django.core.paginator.Page): the current page object
paginator (django.core.paginator.Paginator): the paginator object
#}
<nav class="pagination">
{% if current_page.has_previous() %}
<a href="?page={{ current_page.previous_page_number() }}">
<button>
<i class="fa fa-caret-left"></i>
</button>
</a>
{% else %}
<span class="disabled">{% trans %}Previous{% endtrans %}</span>
<button disabled="disabled"><i class="fa fa-caret-left"></i></button>
{% endif %}
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<span class="active">{{ i }} <span class="sr-only">({% trans %}current{% endtrans %})</span></span>
{% for i in paginator.get_elided_page_range(current_page.number) %}
{% if i == current_page.number %}
<button class="active">{{ i }}</button>
{% elif i == paginator.ELLIPSIS %}
<strong>{{ paginator.ELLIPSIS }}</strong>
{% else %}
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ i }}">{{ i }}</a>
<a href="?page={{ i }}">
<button>{{ i }}</button>
</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next() %}
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.next_page_number() }}">{% trans %}Next{% endtrans %}</a>
{% if current_page.has_next() %}
<a href="?page={{ current_page.next_page_number() }}">
<button>
<i class="fa fa-caret-right"></i>
</button>
</a>
{% else %}
<span class="disabled">{% trans %}Next{% endtrans %}</span>
<button disabled="disabled"><i class="fa fa-caret-right"></i></button>
{% endif %}
{% endif %}
</nav>
{% endmacro %}
{% macro select_all_checkbox(form_id) %}

View File

@ -1,7 +1,7 @@
{% extends "core/base.jinja" %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('user/login.scss') }}">
<link rel="stylesheet" href="{{ static('user/login.scss') }}">
{%- endblock -%}
{% block title %}{% trans %}Register{% endtrans %}{% endblock %}

View File

@ -2,7 +2,7 @@
<html lang="fr">
<head>
<title>{% trans %}Slideshow{% endtrans %}</title>
<link href="{{ scss('com/css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
<link href="{{ static('com/css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="slideshow">

View File

@ -2,7 +2,7 @@
{% from "core/macros.jinja" import show_slots, show_tokens, user_subscription %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('user/user_detail.scss') }}">
<link rel="stylesheet" href="{{ static('user/user_detail.scss') }}">
{%- endblock -%}
{% block title %}

View File

@ -5,7 +5,7 @@
{%- endblock -%}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('user/user_edit.scss') }}">
<link rel="stylesheet" href="{{ static('user/user_edit.scss') }}">
{%- endblock -%}
{% block additional_js %}

View File

@ -2,7 +2,7 @@
{% from "core/macros.jinja" import user_link_with_pict, delete_godfather %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('user/user_godfathers.scss') }}">
<link rel="stylesheet" href="{{ static('user/user_godfathers.scss') }}">
{%- endblock -%}
{% block title %}

View File

@ -3,7 +3,7 @@
{% set depth_max=10 %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('user/user_godfathers.scss') }}">
<link rel="stylesheet" href="{{ static('user/user_godfathers.scss') }}">
{%- endblock -%}
{% block additional_js %}

View File

@ -1,7 +1,7 @@
{% extends "core/base.jinja" %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('user/user_group.scss') }}">
<link rel="stylesheet" href="{{ static('user/user_group.scss') }}">
{%- endblock -%}
{% block content %}

View File

@ -1,7 +1,7 @@
{% extends "core/base.jinja" %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('sas/css/album.scss') }}">
<link rel="stylesheet" href="{{ static('sas/css/album.scss') }}">
{%- endblock -%}
{% block additional_js %}
@ -38,7 +38,7 @@
<h4 x-text="album"></h4>
<div class="photos">
<template x-for="picture in pictures">
<a :href="`/sas/picture/${picture.id}#pict`">
<a :href="`/sas/picture/${picture.id}`">
<div
class="photo"
:class="{not_moderated: !picture.is_moderated}"

View File

@ -1,7 +1,7 @@
{% extends "core/base.jinja" %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('user/user_preferences.scss') }}">
<link rel="stylesheet" href="{{ static('user/user_preferences.scss') }}">
{%- endblock -%}
{% block title %}

View File

@ -1,7 +1,7 @@
{% extends "core/base.jinja" %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('user/user_stats.scss') }}">
<link rel="stylesheet" href="{{ static('user/user_stats.scss') }}">
{%- endblock -%}
{% block title %}

View File

@ -1,7 +1,7 @@
{% extends "core/base.jinja" %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('user/user_tools.scss') }}">
<link rel="stylesheet" href="{{ static('user/user_tools.scss') }}">
{%- endblock -%}
{% block title %}

View File

@ -23,18 +23,10 @@
#
import datetime
from pathlib import Path
import phonenumbers
import sass
from django import template
from django.conf import settings
from django.contrib.staticfiles.finders import find
from django.core.files.base import ContentFile
from django.core.files.storage import storages
from django.template import TemplateSyntaxError
from django.template.defaultfilters import stringfilter
from django.templatetags.static import static
from django.utils.safestring import mark_safe
from django.utils.translation import ngettext
@ -88,23 +80,3 @@ def format_timedelta(value: datetime.timedelta) -> str:
return ngettext(
"%(nb_days)d day, %(remainder)s", "%(nb_days)d days, %(remainder)s", days
) % {"nb_days": days, "remainder": str(remainder)}
@register.simple_tag()
def scss(path):
"""Return path of the corresponding css file after compilation."""
path = Path(path)
if path.suffix != ".scss":
raise TemplateSyntaxError("`scss` tag has been called with a non-scss file")
css_path = path.with_suffix(".css")
if settings.DEBUG:
compile_args = {"filename": find(path)}
if settings.SASS_PRECISION:
compile_args["precision"] = settings.SASS_PRECISION
content = sass.compile(**compile_args)
storage = storages["staticfiles"]
if storage.exists(css_path):
storage.delete(css_path)
storage.save(css_path, ContentFile(content))
return static(str(css_path))

View File

@ -84,7 +84,7 @@ def can_edit_prop(obj: Any, user: User) -> bool:
return False
def can_edit(obj: Any, user: User):
def can_edit(obj: Any, user: User) -> bool:
"""Can the user edit the object.
Args:
@ -105,7 +105,7 @@ def can_edit(obj: Any, user: User):
return can_edit_prop(obj, user)
def can_view(obj: Any, user: User):
def can_view(obj: Any, user: User) -> bool:
"""Can the user see the object.
Args:

View File

@ -73,8 +73,8 @@ class MarkdownInput(Textarea):
context = super().get_context(name, value, attrs)
context["statics"] = {
"js": static("vendored/easymde/easymde.min.js"),
"css": static("vendored/easymde/easymde.min.css"),
"js": static("webpack/easymde-index.js"),
"css": static("webpack/easymde-index.css"),
}
context["translations"] = {
"heading_smaller": _("Heading"),

View File

@ -330,7 +330,7 @@ class Product(models.Model):
Returns:
True if the user can buy this product else False
Warnings:
Warning:
This performs a db query, thus you can quickly have
a N+1 queries problem if you call it in a loop.
Hopefully, you can avoid that if you prefetch the buying_groups :

View File

@ -6,7 +6,7 @@
{% endblock %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('counter/css/activity.scss') }}">
<link rel="stylesheet" href="{{ static('counter/css/activity.scss') }}">
{%- endblock -%}
{% block content %}

View File

@ -1,5 +1,5 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link, paginate %}
{% from 'core/macros.jinja' import user_profile_link, paginate_jinja %}
{% block title %}
{% trans %}Cash register summary list{% endtrans %}
@ -57,7 +57,7 @@
</table>
<br>
{% if is_paginated %}
{{ paginate(page_obj, paginator) }}
{{ paginate_jinja(page_obj, paginator) }}
{% endif %}
{% else %}
{% trans %}There is no cash register summary in this website.{% endtrans %}

View File

@ -1,5 +1,5 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import paginate %}
{% from "core/macros.jinja" import paginate_jinja %}
{% block title %}
{%- trans %}Reloads list{% endtrans %} -- {{ counter.name }}
@ -28,7 +28,7 @@
{%- endfor %}
</table>
{% if is_paginated %}
{{ paginate(page_obj, paginator) }}
{{ paginate_jinja(page_obj, paginator) }}
{% endif %}
{%- endblock %}

View File

@ -1173,12 +1173,16 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
threshold = timezone.now() - timedelta(
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
)
kwargs["last_refillings"] = self.object.refillings.filter(
date__gte=threshold
).order_by("-id")[:20]
kwargs["last_sellings"] = self.object.sellings.filter(
date__gte=threshold
).order_by("-id")[:20]
kwargs["last_refillings"] = (
self.object.refillings.filter(date__gte=threshold)
.select_related("operator", "customer__user")
.order_by("-id")[:20]
)
kwargs["last_sellings"] = (
self.object.sellings.filter(date__gte=threshold)
.select_related("seller", "customer__user")
.order_by("-id")[:20]
)
return kwargs

View File

@ -366,3 +366,35 @@ dans la CI : `djHTML`.
En utilisant conjointement Ruff et djHTML,
on arrive donc à la fois à formater les fichiers
Python et les fichiers relatifs au frontend.
### Npm
[Utiliser npm](https://docs.npmjs.com/cli/v6/commands/npm/)
Npm est un gestionnaire de paquets pour Node.js. C'est l'un des gestionnaires les plus répandus sur le marché et est très complet et utilisé.
Npm possède, tout comme Poetry, la capacité de locker les dépendances au moyen d'un fichier `.lock`. Il a également l'avantage de presque toujours être facilement disponible à l'installation.
Nous l'utilisons ici pour gérer les dépendances JavaScript. Celle-ci sont déclarées dans le fichier `package.json` situé à la racine du projet.
### Webpack
[Utiliser webpack](https://webpack.js.org/concepts/)
Webpack est un bundler de fichiers static. Il nous sert ici à mettre à disposition les dépendances frontend gérées par npm.
Il sert également à intégrer les autres outils JavaScript au workflow du Sith de manière transparente.
Webpack a été choisi pour sa versatilité et sa popularité. C'est un des plus anciens bundler et il est là pour rester.
Le logiciel se configure au moyen du fichier `webpack.config.js` à la racine du projet.
### Babel
[Babel](https://babeljs.io/)
Babel est un outil qui offre la promesse de convertir le code JavaScript moderne en code JavaScript plus ancien sans action de la part du développeur. Il permet de ne pas se soucier de la compatibilité avec les navigateurs et de coder comme si on était toujours sur la dernière version du langage.
Babel est intégré dans Webpack et tout code bundlé par celui-ci est automatiquement converti.
Le logiciel se configure au moyen du fichier `babel.config.json` à la racine du projet.

55
docs/howto/statics.md Normal file
View File

@ -0,0 +1,55 @@
## C'est quoi les fichiers statics ?
Les fichiers statics sont tous les fichiers qui ne sont pas générés par le backend Django et qui sont téléchargés par le navigateur.
Cela comprend les fichiers css, javascript, images et autre.
La [documentation officielle](https://docs.djangoproject.com/fr/4.2/howto/static-files/) est très compréhensive.
Pour faire court, dans chaque module d'application il existe un dossier `static`
où mettre tous ces fichiers. Django se débrouille ensuite pour aller chercher
ce qu'il faut à l'intérieur.
Pour accéder à un fichier static dans un template Jinja il suffit d'utiliser la fonction `static`.
```jinja
{# Exemple pour ajouter sith/core/static/core/base.css #}
<link rel="stylesheet" href="{{ static('core/base.css') }}">
```
## L'intégration des scss
Les scss sont à mettre dans le dossier static comme le reste.
Il n'y a aucune différence avec le reste pour les inclure,
le système se débrouille automatiquement pour les transformer en `.css`
```jinja
{# Exemple pour ajouter sith/core/static/core/base.scss #}
<link rel="stylesheet" href="{{ static('core/style.scss') }}">
```
## L'intégration webpack
Webpack est intégré un peu différement. Le principe est très similaire mais
les fichiers sont à mettre dans un dossier `static/webpack` de l'application à la place.
Pour accéder au fichier, il faut utiliser `static` comme pour le reste mais en ajouter `webpack/` comme prefix.
```jinja
{# Exemple pour ajouter sith/core/webpack/alpine-index.js #}
<script src="{{ static('webpack/alpine-index.js') }}" defer></script>
```
!!!note
Seuls les fichiers se terminant par `index.js` sont exportés par webpack.
Les autres fichiers sont disponibles à l'import dans le JavaScript comme
si ils étaient tous au même niveau.
## Comment ça fonctionne le post processing ?
Le post processing est géré par le module `staticfiles`. Les fichiers sont
compilés à la volée en mode développement.
Pour la production, ils sont compilés uniquement lors du `./manage.py collectstatic`.
Les fichiers générés sont ajoutés dans le dossier `sith/generated`. Celui-ci est
ensuite enregistré comme dossier supplémentaire à collecter dans Django.

View File

@ -0,0 +1 @@
::: staticfiles.apps.StaticFilesConfig

View File

@ -0,0 +1 @@
::: staticfiles.finders

View File

@ -0,0 +1 @@
::: staticfiles.processors

View File

@ -0,0 +1 @@
::: staticfiles.storage

View File

@ -45,7 +45,7 @@ cd /mnt/<la_lettre_du_disque>/vos/fichiers/comme/dhab
!!!note
A ce stade, si vous avez réussi votre installation de `WSL` ou bien qu'il
À ce stade, si vous avez réussi votre installation de `WSL` ou bien qu'il
était déjà installé, vous pouvez effectuer la mise en place du projet en suivant
les instructions pour votre distribution.
@ -70,7 +70,7 @@ cd /mnt/<la_lettre_du_disque>/vos/fichiers/comme/dhab
Puis installez les autres dépendances :
```bash
sudo apt install build-essential libssl-dev libjpeg-dev zlib1g-dev python-dev \
sudo apt install build-essential libssl-dev libjpeg-dev zlib1g-dev python-dev npm \
libffi-dev python-dev-is-python3 pkg-config \
gettext git pipx
@ -84,7 +84,7 @@ cd /mnt/<la_lettre_du_disque>/vos/fichiers/comme/dhab
sudo pacman -S python
sudo pacman -S gcc git gettext pkgconf python-poetry
sudo pacman -S gcc git gettext pkgconf python-poetry npm
```
=== "macOS"
@ -93,7 +93,7 @@ cd /mnt/<la_lettre_du_disque>/vos/fichiers/comme/dhab
Il est également nécessaire d'avoir installé xcode
```bash
brew install git python pipx
brew install git python pipx npm
pipx install poetry
# Pour bien configurer gettext
@ -114,6 +114,9 @@ cd sith3
# Création de l'environnement et installation des dépendances
poetry install
# Configuration du frontend
npm install
# Activation de l'environnement virtuel
poetry shell

View File

@ -66,17 +66,19 @@ sith3/
│ └── ...
├── antispam/ (23)
│ └── ...
├── staticfiles/ (24)
│ └── ...
├── .coveragerc (24)
├── .envrc (25)
├── .coveragerc (25)
├── .envrc (26)
├── .gitattributes
├── .gitignore
├── .mailmap
├── .env.exemple
├── manage.py (26)
├── mkdocs.yml (27)
├── manage.py (27)
├── mkdocs.yml (28)
├── poetry.lock
├── pyproject.toml (28)
├── pyproject.toml (29)
└── README.md
```
</div>
@ -114,16 +116,19 @@ sith3/
19. Application principale du projet, contenant sa configuration.
20. Gestion des stocks des comptoirs.
21. Gestion des cotisations des utilisateurs du site.
22. Fonctionalitées pour gérer le spam.
23. Gestion des trombinoscopes.
24. Fichier de configuration de coverage.
25. Fichier de configuration de direnv.
26. Fichier généré automatiquement par Django. C'est lui
22. Outil pour faciliter la fabrication des trombinoscopes de promo.
23. Fonctionnalités pour gérer le spam.
24. Gestion des statics du site. Override le système de statics de Django.
Ajoute l'intégration du scss et de webpack
de manière transparente pour l'utilisateur.
25. Fichier de configuration de coverage.
26. Fichier de configuration de direnv.
27. Fichier généré automatiquement par Django. C'est lui
qui permet d'appeler des commandes de gestion du projet
avec la syntaxe `python ./manage.py <nom de la commande>`
27. Le fichier de configuration de la documentation,
28. Le fichier de configuration de la documentation,
avec ses plugins et sa table des matières.
28. Le fichier où sont déclarés les dépendances et la configuration
29. Le fichier où sont déclarés les dépendances et la configuration
de certaines d'entre elles.
@ -175,13 +180,16 @@ comme suit :
│ └── ...
├── templates/ (2)
│ └── ...
├── api.py (3)
├── admin.py (4)
├── models.py (5)
├── tests.py (6)
├── schemas.py (7)
├── urls.py (8)
└── views.py (9)
├── static/ (3)
│ └── webpack/ (4)
│ └── ...
├── api.py (5)
├── admin.py (6)
├── models.py (7)
├── tests.py (8)
├── schemas.py (9)
├── urls.py (10)
└── views.py (11)
```
</div>
@ -190,17 +198,19 @@ comme suit :
de mettre à jour la base de données.
cf. [Gestion des migrations](../howto/migrations.md)
2. Dossier contenant les templates jinja utilisés par cette application.
3. Fichier contenant les routes d'API liées à cette application
4. Fichier de configuration de l'interface d'administration.
3. Dossier contenant les fichiers statics (js, css, scss) qui sont récpérée par Django.
4. Dossier contenant du js qui sera process avec webpack. Le contenu sera automatiquement process et accessible comme si ça avait été placé dans le dossier `static/webpack`.
5. Fichier contenant les routes d'API liées à cette application
6. Fichier de configuration de l'interface d'administration.
Ce fichier permet de déclarer les modèles de l'application
dans l'interface d'administration.
5. Fichier contenant les modèles de l'application.
7. Fichier contenant les modèles de l'application.
Les modèles sont des classes Python qui représentent
les tables de la base de données.
6. Fichier contenant les tests de l'application.
7. Schémas de validation de données utilisés principalement dans l'API.
8. Configuration des urls de l'application.
9. Fichier contenant les vues de l'application.
8. Fichier contenant les tests de l'application.
9. Schémas de validation de données utilisés principalement dans l'API.
10. Configuration des urls de l'application.
11. Fichier contenant les vues de l'application.
Dans les plus grosses applications,
ce fichier peut être remplacé par un package
`views` dans lequel les vues sont réparties entre

View File

@ -248,7 +248,7 @@ class BasketItem(AbstractBaseItem):
"""Create a BasketItem with the same characteristics as the
product passed in parameters, with the specified quantity.
Warnings:
Warning:
the basket field is not filled, so you must set
it yourself before saving the model.
"""

View File

@ -6,7 +6,7 @@
{% block head %}
{{ super() -}}
<link rel="stylesheet" href="{{ scss('election/css/election.scss') }}">
<link rel="stylesheet" href="{{ static('election/css/election.scss') }}">
{%- endblock %}
{% block content %}
@ -196,14 +196,13 @@
{% block script %}
{{ super() }}
<script src="{{ static('core/js/shorten.min.js') }}"></script>
<script type="text/javascript">
$('.role_description').shorten({
shorten('.role_description', {
moreText: "{% trans %}Show more{% endtrans %}",
lessText: "{% trans %}Show less{% endtrans %}",
showChars: 50
});
$('.candidate_program').shorten({
shorten('.candidate_program', {
moreText: "{% trans %}Show more{% endtrans %}",
lessText: "{% trans %}Show less{% endtrans %}",
showChars: 200

View File

@ -1,5 +1,5 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import paginate %}
{% from "core/macros.jinja" import paginate_jinja %}
{% block title %}
{%- trans %}Election list{% endtrans %}
@ -46,7 +46,7 @@
</section>
{%- endfor %}
{% if is_paginated %}
{{ paginate(page_obj, paginator) }}
{{ paginate_jinja(page_obj, paginator) }}
{% endif %}
{%- endblock %}

View File

@ -170,7 +170,7 @@ class Forum(models.Model):
def is_owned_by(self, user):
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_FORUM_ADMIN_ID):
if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_FORUM_ADMIN_ID):
return True
try:
m = Forum._club_memberships[self.id][user.id]
@ -273,10 +273,10 @@ class ForumTopic(models.Model):
return self.forum.is_owned_by(user)
def can_be_edited_by(self, user):
return user.can_edit(self.forum)
return user.is_root or user.can_edit(self.forum)
def can_be_viewed_by(self, user):
return user.can_view(self.forum)
return user.is_root or user.can_view(self.forum)
def get_first_unread_message(self, user: User) -> ForumMessage | None:
if not hasattr(user, "forum_infos"):
@ -355,7 +355,7 @@ class ForumMessage(models.Model):
return not self._deleted
def can_be_moderated_by(self, user):
return self.topic.forum.is_owned_by(user) or user.id == self.author.id
return self.topic.forum.is_owned_by(user) or user.id == self.author_id
def get_url(self):
return (

View File

@ -0,0 +1,166 @@
@import "core/static/core/colors";
#forum {
.button {
background-color: rgb(230, 230, 230);
padding: 10px;
font-weight: bold;
border-radius: 5px;
&:hover {
background-color: rgb(211, 211, 211);
}
}
.topic {
border: solid $primary-neutral-color 1px;
padding: 1px;
margin: 1px;
p {
margin: 1px;
font-size: smaller;
}
a {
color: $black-color;
}
a:hover {
text-decoration: underline;
}
}
.tools {
font-size: x-small;
border: none;
font-weight: bold;
a {
padding: 1px;
}
}
.title {
font-size: small;
font-weight: bold;
padding: 2px;
}
.last_message span {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
display: block;
}
.forum {
background: $primary-neutral-light-color;
padding: 1px;
margin: 1px;
p {
margin: 1px;
font-size: smaller;
}
a {
color: $black-color;
}
a:hover {
text-decoration: underline;
}
}
.search_bar {
margin: 10px 0;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.search_check {
margin-left: 10px;
}
.search_bouton {
margin-left: 10px;
}
.category {
margin-top: 5px;
background: $secondary-color;
color: white;
border-radius: 10px 10px 0 0;
.title {
text-transform: uppercase;
}
}
.message {
padding: 15px;
margin: 20px;
display: flex;
flex-direction: column;
gap: 10px;
background: $secondary-neutral-light-color;
border-radius: 5px;
border: 1px darken($secondary-neutral-light-color, 10%) solid;
&:nth-child(odd) {
background: $primary-neutral-light-color;
}
.message-header {
display: flex;
gap: 20px;
border-bottom: 0.0625rem grey dotted;
padding-bottom: 10px;
img {
width: 50px;
height: 50px;
border-radius: 50%;
}
.message-metadata {
display: flex;
flex-direction: column;
justify-content: center;
}
.message-options {
flex: 2;
display: flex;
justify-content: right;
gap: 15px;
}
}
.message-content {
padding: 0 20px;
&.delete > * {
margin-top: 0;
}
.markdown blockquote h5 {
padding-top: 0;
margin-top: 0;
}
}
.forum-signature {
margin: 0;
font-size: small;
font-style: italic;
border-top: 0.0625rem grey dotted;
}
&.unread {
background: #e9eea1;
}
}
}

View File

@ -5,6 +5,10 @@
{% trans %}Favorite topics{% endtrans %}
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('forum/css/forum.scss') }}">
{% endblock %}
{% block content %}
<p>
<a href="{{ url('forum:main') }}">Forum</a> >

View File

@ -5,6 +5,10 @@
{{ forum }}
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('forum/css/forum.scss') }}">
{% endblock %}
{% block content %}
{{ display_breadcrumb(forum) }}
<div id="forum">

View File

@ -5,6 +5,10 @@
{% trans %}Last unread messages{% endtrans %}
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('forum/css/forum.scss') }}">
{% endblock %}
{% block content %}
<p>
<a href="{{ url('forum:main') }}">Forum</a> >

View File

@ -97,73 +97,81 @@
{% endmacro %}
{% macro display_message(m, user, unread=False) %}
{% if user.can_view(m) %}
<div id="msg_{{ m.id }}" class="message {% if unread %}unread{% endif %}">
<div class="msg_author {% if m.deleted %}deleted{% endif %}">
{% set user_is_admin = m.topic.is_owned_by(user) %}
<article id="msg_{{ m.id }}" class="message {% if unread %}unread{% endif %}">
{% if user_is_admin or not m._deleted %}
<div class="message-header">
{% if m.author.avatar_pict %}
<img src="{{ m.author.avatar_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}" id="picture" />
{% elif m.author.profile_pict %}
<img src="{{ m.author.profile_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}" id="picture" />
{% else %}
<img src="{{ static('core/img/unknown.jpg') }}" alt="{% trans %}Profile{% endtrans %}" id="picture" />
{% endif %}
<br/>
<strong><a href="{{ m.author.get_absolute_url() }}">{{ m.author.get_short_name() }}</a></strong>
</div>
<div class="msg_content {% if m.deleted %}deleted{% endif %}" {% if m.id == first_unread_message_id %}id="first_unread"{% endif %}>
<div class="msg_header">
<div class="ib w_big title">
<a href="{{ m.get_absolute_url() }}">
{{ m.date|localtime|date(DATETIME_FORMAT) }}
{{ m.date|localtime|time(DATETIME_FORMAT) }}
{%- if m.title -%}
- {{ m.title }}
{%- endif -%}
<div class="message-metadata">
<a href="{{ m.author.get_absolute_url() }}">
<strong>{{ m.author.get_short_name() }}</strong>
</a>
<a href="{{ m.get_absolute_url() }}">
{{ m.date|localtime|date(DATETIME_FORMAT) }}
{{ m.date|localtime|time(DATETIME_FORMAT) }}
</a>
</div>
<div class="message-options">
<a href="{{ url('forum:new_message', topic_id=m.topic.id) }}?quote_id={{ m.id }}">
<i class="fa fa-quote-right"></i>
</a>
{% if user.can_edit(m) %}
<a href="{{ url('forum:edit_message', message_id=m.id) }}">
<i class="fa fa-pencil"></i>
</a>
</div>
<div class="ib w_small">
<span><a href="{{ m.get_absolute_url() }}">#{{ m.id }}</a></span>
<br/>
<span><a href="{{ url('forum:new_message', topic_id=m.topic.id) }}?quote_id={{ m.id }}">
{% trans %}Reply as quote{% endtrans %}</a></span>
{% if user.can_edit(m) %}
<span> <a href="{{ url('forum:edit_message', message_id=m.id) }}">{% trans %}Edit{% endtrans %}</a></span>
{% endif %}
{% if m.can_be_moderated_by(user) %}
{% if m.deleted %}
<span> <a href="{{ url('forum:undelete_message', message_id=m.id) }}">{% trans %}Undelete{% endtrans %}</a></span>
{% else %}
<span> <a href="{{ url('forum:delete_message', message_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></span>
{% endif %}
{% endif %}
</div>
</div>
<hr>
<div>
{{ m.message|markdown }}
{% endif %}
{% if user_is_admin and m._deleted %}
<span>
<a href="{{ url('forum:undelete_message', message_id=m.id) }}">
{% trans %}Undelete{% endtrans %}
</a>
</span>
{% endif %}
{% if not m._deleted and (user_is_admin or user.id == m.author_id) %}
<span>
<a href="{{ url('forum:delete_message', message_id=m.id) }}">
<i class="fa fa-trash"></i>
</a>
</span>
{% endif %}
</div>
</div>
{% endif %} {# close `user.can_view(m) or user_is_admin` #}
{% if user.can_view(m) %}
<div
class="message-content {%- if m.deleted -%}deleted{%- endif -%}"
{%- if m.id == first_unread_message_id -%}id="first_unread"{%- endif -%}
>
{{ m.message|markdown }}
{% if m.can_be_moderated_by(user) %}
<ul class="msg_meta">
{% for meta in m.metas.select_related('user').order_by('id') %}
<li style="background: {% if m.author == meta.user %}#bfffbf{% else %}#ffffbf{% endif %}">
{{ meta.get_action_display() }} {{ meta.user.get_short_name() }}
{{ meta.get_action_display() }} {{ meta.user.get_short_name() }}
{% trans %} at {% endtrans %}{{ meta.date|localtime|time(DATETIME_FORMAT) }}
{% trans %} the {% endtrans %}{{ meta.date|localtime|date(DATETIME_FORMAT)}}</li>
{% trans %} the {% endtrans %}{{ meta.date|localtime|date(DATETIME_FORMAT)}}
</li>
{% endfor %}
</ul>
{% endif %}
<div class="forum_signature">{{ m.author.forum_signature|markdown }}</div>
</div>
</div>
{% else %}
<div id="msg_{{ m.id }}" class="message">
<div class="msg_author deleted">
</div>
<div class="msg_content deleted">
<p class="ib w_big">{% trans %}Deleted or unreadable message.{% endtrans %}</p>
<p class="ib w_small">{{ m.date|localtime|date(DATETIME_FORMAT) }} {{ m.date|localtime|time(DATETIME_FORMAT) }}</p>
</div>
</div>
{% endif %}
{{ m.mark_as_read(user) or "" }}
{% if m.author.forum_signature %}
<div class="forum-signature">{{ m.author.forum_signature|markdown }}</div>
{% endif %}
{% else %}
<div class="message-content delete">
<p>{% trans %}Deleted or unreadable message.{% endtrans %}</p>
</div>
{% endif %}
</article>
{{ m.mark_as_read(user) or "" }}
{% endmacro %}
{% macro display_search_bar(request) %}

View File

@ -6,6 +6,10 @@
{% trans %}Forum{% endtrans %}
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('forum/css/forum.scss') }}">
{% endblock %}
{% block content %}
<p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a> >

View File

@ -9,6 +9,11 @@
{% endif %}
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('forum/css/forum.scss') }}">
{% endblock %}
{% block content %}
{% if topic %}
{{ display_search_bar(request) }}

View File

@ -2,6 +2,11 @@
{% from 'forum/macros.jinja' import display_message, display_breadcrumb, display_search_bar %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('forum/css/forum.scss') }}">
{% endblock %}
{% block content %}
<div id="forum">
{{ display_search_bar(request) }}

View File

@ -1,28 +1,15 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %}
{% from 'forum/macros.jinja' import display_message, display_breadcrumb, display_search_bar %}
{% from 'core/macros.jinja' import paginate_jinja %}
{% block title %}
{{ topic }}
{% endblock %}
{% block head %}
{{ super() }}
<style type="text/css" media="all">
.topic {
border: solid skyblue 1px;
padding: 2px;
margin: 2px;
}
.forum {
background: lightblue;
padding: 2px;
margin: 2px;
}
.category {
background: skyblue;
}
</style>
{% block additional_css %}
<link rel="stylesheet" href="{{ static('forum/css/forum.scss') }}">
{% endblock %}
{% block content %}
@ -40,30 +27,24 @@
</p>
{{ display_search_bar(request) }}
<p style="text-align: right; background: #d8e7f3;">
{% for p in msgs.paginator.page_range %}
<span class="ib" style="background: {% if p == msgs.number %}white{% endif %}; margin: 0;"><a href="?page={{ p }}">{{ p }}</a></span>
{% endfor %}
</p>
{{ paginate_jinja(msgs, msgs.paginator) }}
{% for m in msgs %}
{% if m.id == first_unread_message_id %}
<span id="first_unread"></span>
{% endif %}
{% if m.id >= first_unread_message_id %}
{{ display_message(m, user, True) }}
{% else %}
{{ display_message(m, user, False) }}
{% endif %}
{% endfor %}
<main class="message-list">
{% for m in msgs %}
{% if m.id == first_unread_message_id %}
<span id="first_unread"></span>
{% endif %}
{% if m.id >= first_unread_message_id %}
{{ display_message(m, user, True) }}
{% else %}
{{ display_message(m, user, False) }}
{% endif %}
{% endfor %}
</main>
<p><a class="ib button" href="{{ url('forum:new_message', topic_id=topic.id) }}">{% trans %}Reply{% endtrans %}</a></p>
<p style="text-align: right; background: #d8e7f3;">
{% for p in msgs.paginator.page_range %}
<span class="ib" style="background: {% if p == msgs.number %}white{% endif %}; margin: 0;"><a href="?page={{ p }}">{{ p }}</a></span>
{% endfor %}
</p>
{{ paginate_jinja(msgs, msgs.paginator) }}
</div>
{% endblock %}

View File

@ -306,14 +306,15 @@ class ForumTopicDetailView(CanViewMixin, DetailView):
queryset = ForumTopic.objects.select_related("forum__parent")
def get_context_data(self, **kwargs):
topic: ForumTopic = self.object
kwargs = super().get_context_data(**kwargs)
msg = self.object.get_first_unread_message(self.request.user)
msg = topic.get_first_unread_message(self.request.user)
if msg is None:
kwargs["first_unread_message_id"] = math.inf
else:
kwargs["first_unread_message_id"] = msg.id
paginator = Paginator(
self.object.messages.select_related("author__avatar_pict")
topic.messages.select_related("author__avatar_pict", "topic__forum")
.prefetch_related("topic__forum__edit_groups", "readers")
.order_by("date"),
settings.SITH_FORUM_PAGE_LENGTH,

View File

@ -1,5 +1,5 @@
{% from "core/macros.jinja" import user_mini_profile, paginate %}
{% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_mini_profile, paginate_jinja %}
{% block title %}
{% trans %}Search user{% endtrans %}
@ -18,7 +18,9 @@
{% endfor %}
</div>
{{ paginate(page_obj, paginator) }}
{% if page_obj.has_other_pages() %}
{{ paginate_jinja(page_obj, paginator) }}
{% endif %}
<hr>
{% endif %}
<h2>{% trans %}Search user{% endtrans %}</h2>

View File

@ -71,6 +71,7 @@ nav:
- L'ORM de Django: howto/querysets.md
- Gérer les migrations: howto/migrations.md
- Gérer les traductions: howto/translation.md
- Gérer les statics: howto/statics.md
- Configurer pour la production: howto/prod.md
- Ajouter un logo de promo: howto/logo.md
- Ajouter une cotisation: howto/subscriptions.md
@ -129,6 +130,11 @@ nav:
- reference/sas/models.md
- reference/sas/views.md
- reference/sas/schemas.md
- staticfiles:
- reference/staticfiles/apps.md
- reference/staticfiles/storage.md
- reference/staticfiles/finders.md
- reference/staticfiles/processors.md
- stock:
- reference/stock/models.md
- reference/stock/views.md

5527
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "sith",
"version": "3",
"description": "Le web Sith de l'AE",
"main": "index.js",
"scripts": {
"compile": "webpack --mode production",
"compile-dev": "webpack --mode development",
"serve": "webpack --mode development --watch"
},
"keywords": [],
"author": "",
"license": "GPL-3.0-only",
"sideEffects": [
".css"
],
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"babel-loader": "^9.2.1",
"expose-loader": "^5.0.0",
"mini-css-extract-plugin": "^2.9.1",
"source-map-loader": "^5.0.0",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0"
},
"dependencies": {
"alpinejs": "^3.14.1",
"easymde": "^2.18.0",
"glob": "^11.0.0",
"jquery": "^3.7.1",
"jquery-ui": "^1.14.0",
"jquery.shorten": "^1.0.0"
}
}

View File

@ -6,8 +6,7 @@
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ scss('pedagogy/css/pedagogy.scss') }}">
<link rel="stylesheet" href="{{ scss('core/pagination.scss') }}">
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
{% endblock %}
{% block head %}
@ -215,4 +214,4 @@
}))
})
</script>
{% endblock content %}
{% endblock content %}

View File

@ -1,5 +1,5 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import paginate %}
{% from "core/macros.jinja" import paginate_jinja %}
{% block title %}
{% trans %}Operation logs{% endtrans %}
@ -28,5 +28,5 @@
</table>
<br>
{{ paginate(page_obj, paginator) }}
{{ paginate_jinja(page_obj, paginator) }}
{% endblock content %}

View File

@ -134,13 +134,15 @@ def merge_users(u1: User, u2: User) -> User:
return u1
def delete_all_forum_user_messages(user, moderator, *, verbose=False):
def delete_all_forum_user_messages(
user: User, moderator: User, *, verbose: bool = False
):
"""Soft delete all messages of a user.
Args:
user: the user to delete messages from
moderator: the one marked as the moderator.
verbose: it True, print the deleted messages
user: core.models.User the user to delete messages from
moderator: core.models.User the one marked as the moderator.
verbose: bool if True, print the deleted messages
"""
for message in user.forum_messages.all():
if message.is_deleted():

View File

@ -66,7 +66,7 @@ class PictureQuerySet(models.QuerySet):
def viewable_by(self, user: User) -> Self:
"""Filter the pictures that this user can view.
Warnings:
Warning:
Calling this queryset method may add several additional requests.
"""
if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
@ -178,7 +178,7 @@ class AlbumQuerySet(models.QuerySet):
def viewable_by(self, user: User) -> Self:
"""Filter the albums that this user can view.
Warnings:
Warning:
Calling this queryset method may add several additional requests.
"""
if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):

View File

@ -137,7 +137,12 @@ document.addEventListener("alpine:init", () => {
this.current_picture = this.pictures.find(
(i) => i.id === first_picture_id,
);
this.$watch("current_picture", () => this.update_picture());
this.$watch("current_picture", (current, previous) => {
if (current === previous){ /* Avoid recursive updates */
return;
}
this.update_picture();
});
window.addEventListener("popstate", async (event) => {
if (!event.state || event.state.sas_picture_id === undefined) {
return;

View File

@ -2,8 +2,7 @@
{% from 'core/macros.jinja' import paginate_alpine %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('sas/css/album.scss') }}">
<link rel="stylesheet" href="{{ scss('core/pagination.scss') }}">
<link rel="stylesheet" href="{{ static('sas/css/album.scss') }}">
{%- endblock -%}
{% block title %}
@ -64,7 +63,7 @@
<h4>{% trans %}Pictures{% endtrans %}</h4>
<div class="photos" :aria-busy="loading">
<template x-for="picture in pictures.results">
<a :href="`/sas/picture/${picture.id}#pict`">
<a :href="`/sas/picture/${picture.id}`">
<div
class="photo"
:class="{not_moderated: !picture.is_moderated}"

View File

@ -1,7 +1,7 @@
{% extends "core/base.jinja" %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('sas/css/album.scss') }}">
<link rel="stylesheet" href="{{ static('sas/css/album.scss') }}">
{%- endblock -%}
{% block title %}

View File

@ -1,7 +1,7 @@
{% extends "core/base.jinja" %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('sas/css/picture.scss') }}">
<link rel="stylesheet" href="{{ static('sas/css/picture.scss') }}">
{%- endblock -%}
{%- block additional_js -%}

View File

@ -1,39 +0,0 @@
#!/usr/bin/env python3
#
# Copyright 2016,2017
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.conf import settings
from django.contrib.staticfiles.finders import FileSystemFinder
from django.core.files.storage import FileSystemStorage
class ScssFinder(FileSystemFinder):
"""Find static *.css files compiled on the fly."""
def __init__(self, apps=None, *args, **kwargs):
location = settings.STATIC_ROOT
self.locations = [("", location)]
self.storages = {}
filesystem_storage = FileSystemStorage(location=location)
filesystem_storage.prefix = self.locations[0][0]
self.storages[location] = filesystem_storage

View File

@ -74,7 +74,7 @@ INSTALLED_APPS = (
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"staticfiles",
"django.contrib.sites",
"honeypot",
"django_jinja",
@ -163,7 +163,6 @@ TEMPLATES = [
"ProductType": "counter.models.ProductType",
"timezone": "django.utils.timezone",
"get_sith": "com.views.sith",
"scss": "core.templatetags.renderer.scss",
},
"bytecode_cache": {
"name": "default",
@ -268,9 +267,8 @@ STATIC_ROOT = BASE_DIR / "static"
# Static files finders which allow to see static folder in all apps
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"staticfiles.finders.GeneratedFilesFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
"sith.finders.ScssFinder",
]
STORAGES = {
@ -278,7 +276,7 @@ STORAGES = {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "sith.storage.SithStorage",
"BACKEND": "staticfiles.storage.ManifestPostProcessingStorage",
},
}
@ -743,14 +741,8 @@ SITH_FRONT_DEP_VERSIONS = {
"https://github.com/gildas-lormeau/zip.js": "2.7.47",
"https://github.com/jimmywarting/native-file-system-adapter": "3.0.1",
"https://github.com/chartjs/Chart.js/": "2.6.0",
"https://github.com/Ionaru/easy-markdown-editor/": "2.18.0",
"https://github.com/FortAwesome/Font-Awesome/": "4.7.0",
"https://github.com/jquery/jquery/": "3.6.2",
"https://github.com/sethmcl/jquery-ui/": "1.11.1",
"https://github.com/viralpatel/jquery.shorten/": "",
"https://github.com/getsentry/sentry-javascript/": "8.26.0",
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
"https://github.com/alpinejs/alpine": "3.14.1",
"https://github.com/cytoscape/cytoscape.js": "3.30.2 ",
"https://github.com/cytoscape/cytoscape.js-cxtmenu": "3.5.0",
"https://github.com/cytoscape/cytoscape.js-klay": "3.1.4",

View File

@ -1,65 +0,0 @@
import logging
import rjsmin
import sass
from django.conf import settings
from django.contrib.staticfiles.storage import (
ManifestStaticFilesStorage,
)
from django.core.files.storage import Storage
class SithStorage(ManifestStaticFilesStorage):
def _compile_scss(self):
to_exec = list(settings.STATIC_ROOT.rglob("*.scss"))
if len(to_exec) == 0:
return
for file in to_exec:
# remove existing css files that will be replaced
# keeping them while compiling the scss would break
# import statements resolution
css_file = file.with_suffix(".css")
if css_file.exists():
css_file.unlink()
scss_paths = [p.resolve() for p in to_exec if p.suffix == ".scss"]
base_args = {"output_style": "compressed", "precision": settings.SASS_PRECISION}
compiled_files = {
p: sass.compile(filename=str(p), **base_args) for p in scss_paths
}
for file, scss in compiled_files.items():
file.replace(file.with_suffix(".css")).write_text(scss)
# once the files are compiled, the manifest must be updated
# to have the right suffix
new_entries = {
k.replace(".scss", ".css"): self.hashed_files.pop(k).replace(
".scss", ".css"
)
for k in list(self.hashed_files.keys())
if k.endswith(".scss")
}
self.hashed_files.update(new_entries)
self.save_manifest()
@staticmethod
def _minify_js():
to_exec = [
p for p in settings.STATIC_ROOT.rglob("*.js") if ".min" not in p.suffixes
]
for path in to_exec:
p = path.resolve()
minified = rjsmin.jsmin(p.read_text())
p.write_text(minified)
logging.getLogger("main").info(f"Minified {path}")
def post_process(
self, paths: dict[str, tuple[Storage, str]], *, dry_run: bool = False
):
# Whether we get the files that were processed by ManifestFilesMixin
# by calling super() or whether we get them from the manifest file
# makes no difference - we have to open the manifest file anyway
# because we need to update the paths stored inside it.
yield from super().post_process(paths, dry_run)
self._compile_scss()
self._minify_js()

1
staticfiles/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
generated/

0
staticfiles/__init__.py Normal file
View File

29
staticfiles/apps.py Normal file
View File

@ -0,0 +1,29 @@
from pathlib import Path
from django.contrib.staticfiles.apps import StaticFilesConfig
GENERATED_ROOT = Path(__file__).parent.resolve() / "generated"
IGNORE_PATTERNS_WEBPACK = ["webpack/*"]
IGNORE_PATTERNS_SCSS = ["*.scss"]
IGNORE_PATTERNS = [
*StaticFilesConfig.ignore_patterns,
*IGNORE_PATTERNS_WEBPACK,
*IGNORE_PATTERNS_SCSS,
]
# We override the original staticfiles app according to https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/#customizing-the-ignored-pattern-list
# However, this is buggy and requires us to have an exact naming of the class like this to be detected
# Also, it requires to create all commands in management/commands again or they don't get detected by django
# Workaround originates from https://stackoverflow.com/a/78724835/12640533
class StaticFilesConfig(StaticFilesConfig):
"""
Application in charge of processing statics files.
It replaces the original django staticfiles
It integrates scss files and webpack.
It makes sure that statics are properly collected and that they are automatically
when using the development server.
"""
ignore_patterns = IGNORE_PATTERNS
name = "staticfiles"

36
staticfiles/finders.py Normal file
View File

@ -0,0 +1,36 @@
import os
from django.contrib.staticfiles import utils
from django.contrib.staticfiles.finders import FileSystemFinder
from django.core.files.storage import FileSystemStorage
from staticfiles.apps import GENERATED_ROOT, IGNORE_PATTERNS_WEBPACK
class GeneratedFilesFinder(FileSystemFinder):
"""Find generated and regular static files"""
def __init__(self, app_names=None, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add GENERATED_ROOT after adding everything in settings.STATICFILES_DIRS
self.locations.append(("", GENERATED_ROOT))
generated_storage = FileSystemStorage(location=GENERATED_ROOT)
generated_storage.prefix = ""
self.storages[GENERATED_ROOT] = generated_storage
def list(self, ignore_patterns: list[str]):
# List all files availables
for _, root in self.locations:
# Skip nonexistent directories.
if not os.path.isdir(root):
continue
ignored = ignore_patterns
# We don't want to ignore webpack files in the generated folder
if root == GENERATED_ROOT:
ignored = list(set(ignored) - set(IGNORE_PATTERNS_WEBPACK))
storage = self.storages[root]
for path in utils.get_files(storage, ignored):
yield path, storage

View File

@ -0,0 +1,60 @@
import shutil
from pathlib import Path
from django.contrib.staticfiles.finders import get_finders
from django.contrib.staticfiles.management.commands.collectstatic import (
Command as CollectStatic,
)
from staticfiles.apps import GENERATED_ROOT, IGNORE_PATTERNS_SCSS
from staticfiles.processors import Scss, Webpack
class Command(CollectStatic):
"""Integrate webpack and css compilation to collectstatic"""
def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
"--clear-generated",
action="store_true",
help="Delete the generated folder after collecting statics.",
)
def set_options(self, **options):
super().set_options(**options)
self.clear_generated = options["clear_generated"]
def collect_scss(self) -> list[Scss.CompileArg]:
files: list[Scss.CompileArg] = []
for finder in get_finders():
for path, storage in finder.list(
set(self.ignore_patterns) - set(IGNORE_PATTERNS_SCSS)
):
path = Path(path)
if path.suffix != ".scss":
continue
files.append(
Scss.CompileArg(absolute=storage.path(path), relative=path)
)
return files
def collect(self):
if self.clear: # Clear generated folder
shutil.rmtree(GENERATED_ROOT, ignore_errors=True)
def to_path(location: str | tuple[str, str]) -> Path:
if isinstance(location, tuple):
# staticfiles can be in a (prefix, path) format
_, location = location
return Path(location)
Scss.compile(self.collect_scss())
Webpack.compile()
collected = super().collect()
if self.clear_generated:
shutil.rmtree(GENERATED_ROOT, ignore_errors=True)
return collected

View File

@ -0,0 +1,2 @@
# ruff: noqa: F401
from django.contrib.staticfiles.management.commands.findstatic import Command

View File

@ -0,0 +1,22 @@
import os
from django.conf import settings
from django.contrib.staticfiles.management.commands.runserver import (
Command as Runserver,
)
from django.utils.autoreload import DJANGO_AUTORELOAD_ENV
from staticfiles.processors import Webpack
class Command(Runserver):
"""Light wrapper around the statics runserver that integrates webpack auto bundling"""
def run(self, **options):
# Only run webpack server when debug is enabled
# Also protects from re-launching the server if django reloads it
if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and settings.DEBUG:
with Webpack.runserver():
super().run(**options)
return
super().run(**options)

View File

73
staticfiles/processors.py Normal file
View File

@ -0,0 +1,73 @@
import logging
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
import rjsmin
import sass
from django.conf import settings
from staticfiles.apps import GENERATED_ROOT
class Webpack:
@staticmethod
def compile():
"""Bundle js files with webpack for production."""
process = subprocess.Popen(["npm", "run", "compile"])
process.wait()
if process.returncode:
raise RuntimeError(f"Webpack failed with returncode {process.returncode}")
@staticmethod
def runserver() -> subprocess.Popen:
"""Bundle js files automatically in background when called in debug mode."""
logging.getLogger("django").info("Running webpack server")
return subprocess.Popen(["npm", "run", "serve"])
class Scss:
@dataclass
class CompileArg:
absolute: Path # Absolute path to the file
relative: Path # Relative path inside the folder it has been collected
@staticmethod
def compile(files: CompileArg | Iterable[CompileArg]):
"""Compile scss files to css files."""
# Generate files inside the generated folder
# .css files respects the hierarchy in the static folder it was found
# This converts arg.absolute -> generated/{arg.relative}.scss
# Example:
# app/static/foo.scss -> generated/foo.css
# app/static/bar/foo.scss -> generated/bar/foo.css
# custom/location/bar/foo.scss -> generated/bar/foo.css
if isinstance(files, Scss.CompileArg):
files = [files]
base_args = {"output_style": "compressed", "precision": settings.SASS_PRECISION}
compiled_files = {
file.relative.with_suffix(".css"): sass.compile(
filename=str(file.absolute), **base_args
)
for file in files
}
for file, content in compiled_files.items():
dest = GENERATED_ROOT / file
dest.parent.mkdir(exist_ok=True, parents=True)
dest.write_text(content)
class JS:
@staticmethod
def minify():
to_exec = [
p for p in settings.STATIC_ROOT.rglob("*.js") if ".min" not in p.suffixes
]
for path in to_exec:
p = path.resolve()
minified = rjsmin.jsmin(p.read_text())
p.write_text(minified)
logging.getLogger("main").info(f"Minified {path}")

41
staticfiles/storage.py Normal file
View File

@ -0,0 +1,41 @@
from pathlib import Path
from django.conf import settings
from django.contrib.staticfiles.finders import find
from django.contrib.staticfiles.storage import (
ManifestStaticFilesStorage,
)
from django.core.files.storage import Storage
from staticfiles.processors import JS, Scss
class ManifestPostProcessingStorage(ManifestStaticFilesStorage):
def url(self, name: str, *, force: bool = False) -> str:
"""Get the URL for a file, convert .scss calls to .css ones"""
# This name swap has to be done here
# Otherwise, the manifest isn't aware of the file and can't work properly
path = Path(name)
if path.suffix == ".scss":
# Compile scss files automatically in debug mode
if settings.DEBUG:
Scss.compile(
[
Scss.CompileArg(absolute=Path(p), relative=Path(name))
for p in find(name, all=True)
]
)
name = str(path.with_suffix(".css"))
return super().url(name, force=force)
def post_process(
self, paths: dict[str, tuple[Storage, str]], *, dry_run: bool = False
):
# Whether we get the files that were processed by ManifestFilesMixin
# by calling super() or whether we get them from the manifest file
# makes no difference - we have to open the manifest file anyway
# because we need to update the paths stored inside it.
yield from super().post_process(paths, dry_run)
if not dry_run:
JS.minify()

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