Compare commits

..
Author SHA1 Message Date
thomas girodandGitHub d914fa0658 Merge pull request #1463 from ae-utbm/taiste
Pre school year release
2026-08-25 22:37:49 +02:00
17 changed files with 139 additions and 156 deletions
@@ -7,7 +7,7 @@
<form
hx-post="{{ url('club:club_new_members', club_id=club.id) }}"
hx-disable="find input[type='submit']"
hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML"
hx-target="#member-fragment-container"
id="add_club_members_form"
+5 -10
View File
@@ -6,15 +6,10 @@
* for more efficient tree-shaking and gzip compression.
*/
// Must be loaded before Apline
import htmx from "htmx.org";
import "htmx.org/dist/ext/hx-alpine-compat.js";
import "htmx.org/dist/ext/hx-prompt.js";
import "htmx.org/dist/ext/hx-download.js";
import sort from "@alpinejs/sort";
import Alpine from "alpinejs";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import htmx from "htmx.org";
import { limitedChoices } from "#core:alpine/limited-choices";
import { expireOldStorage } from "#core:core/localstorage";
import { default as navbar } from "#core:core/navbar";
@@ -49,16 +44,16 @@ polyfillCountryFlagEmojis();
* HTMX
*/
document.body.addEventListener(
"htmx:before:request" as keyof HTMLElementEventMap,
"htmx:beforeRequest" as keyof HTMLElementEventMap,
(event) => {
(event as CustomEvent).detail.ctx.target.ariaBusy = true;
(event as CustomEvent).detail.target.ariaBusy = true;
},
);
document.body.addEventListener(
"htmx:before:swap" as keyof HTMLElementEventMap,
"htmx:beforeSwap" as keyof HTMLElementEventMap,
(event) => {
(event as CustomEvent).detail.ctx.target.ariaBusy = null;
(event as CustomEvent).detail.target.ariaBusy = null;
},
);
@@ -1,6 +1,6 @@
<form
hx-post="{{ url("core:user_visibility_fragment", user_id=form.instance.id) }}"
hx-disable="find input[type='submit']"
hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML" x-data="{ isViewable: {{ form.is_viewable.value()|tojson }} }"
>
{% for message in messages %}
-16
View File
@@ -141,22 +141,6 @@ class TestSearchUsersView(TestSearchUsers):
response = self.client.get(reverse("core:search"))
assert response.status_code == 200
def test_search_with_whitelist_unique(self):
"""Test that when a user has a whitelist and appears in the results,
it appears only once.
This is a regression test (cf #1463)
"""
user = subscriber_user.make(is_viewable=False)
user.whitelisted_users.add(
*subscriber_user.make(_quantity=4, _bulk_create=True)
)
self.client.force_login(user)
response = self.client.get(
reverse("core:search", query={"query": user.last_name})
)
assert response.context_data["users"] == [user]
@pytest.mark.django_db
def test_user_account_not_found(client: Client):
-1
View File
@@ -65,7 +65,6 @@ class SearchView(LoginRequiredMixin, TemplateView):
UserFilterSchema(search=query)
.filter(User.objects.viewable_by(self.request.user))
.order_by(F("last_login").desc(nulls_last=True))
.distinct()
)
clubs = list(Club.objects.filter(name__icontains=query)[:5])
return super().get_context_data(**kwargs) | {"users": users, "clubs": clubs}
@@ -28,23 +28,18 @@ export class ProductAjaxSelect extends AjaxSelect {
return [];
}
// In the context in which this method is called, `this` might be shadowed
// We need to call it explicitly from the class itself
private static getName(
item: SimpleProductSchema,
sanitize: typeof escape_html,
): string {
private getName(item: SimpleProductSchema, sanitize: typeof escape_html): string {
return item.code ? `${sanitize(item.code)} - ${sanitize(item.name)}` : item.name;
}
protected renderOption(item: SimpleProductSchema, sanitize: typeof escape_html) {
return `<div class="select-item">
<span class="select-item-text">${ProductAjaxSelect.getName(item, sanitize)}</span>
<span class="select-item-text">${this.getName(item, sanitize)}</span>
</div>`;
}
protected renderItem(item: SimpleProductSchema, sanitize: typeof escape_html) {
return `<span>${ProductAjaxSelect.getName(item, sanitize)}</span>`;
return `<span>${this.getName(item, sanitize)}</span>`;
}
}
@@ -1,10 +1,5 @@
<div id="student_card_form">
<form
hx-post="{{ action }}"
hx-swap="outerHTML"
hx-target="#student_card_form"
hx-disable="input[type='submit']"
>
<form hx-post="{{ action }}" hx-swap="outerHTML" hx-target="#student_card_form">
{% csrf_token %}
<p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p>
<input type="submit" value="{% trans %}Confirm{% endtrans %}" />
+1 -1
View File
@@ -217,7 +217,7 @@ C'est une technologie simple et puissante qui se veut comme le jQuery du web mod
### Htmx
[Site officiel](https://four.htmx.org/)
[Site officiel](https://htmx.org/)
En plus de AlpineJS, linteractivité sur le site est augmentée via Htmx.
C'est une librairie js qui s'utilise également au moyen d'attributs HTML à
@@ -41,7 +41,7 @@
hx-post="{{ url("election:apply_result", election_id=form.election.id) }}"
hx-swap="outerHTML"
hx-target="#apply-election-result-fragment"
hx-disable="find input[type='submit']"
hx-disabled-elt="find input[type='submit']"
>
{% csrf_token %}
{{ form }}
+5 -11
View File
@@ -30,7 +30,7 @@
"easymde": "^2.21.0",
"glob": "^13.0.6",
"html2canvas": "^1.4.1",
"htmx.org": "^4.0.0",
"htmx.org": "^2.0.10",
"js-cookie": "^3.0.8",
"lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1",
@@ -3801,16 +3801,10 @@
}
},
"node_modules/htmx.org": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-4.0.0.tgz",
"integrity": "sha512-T/171FUY93Kdfp8t+DnHdk45QvKRiBhVhhrwSzrXgUi4pHKvhp77dUA/qg8FAjsFWPIHNbmUuIdCrcVHuiZWng==",
"license": "BSD-0-Clause",
"workspaces": [
"ext/*"
],
"bin": {
"upgrade-check": "dist/scripts/upgrade-check.js"
}
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz",
"integrity": "sha512-kdeJe7ZVwaS6QMz/ebBIVtZdpwen6L0OQ5GOhPV9MKBb196TCZeZu4yA7ZIQsaLKv7EpXz+So7KSXNuHXhj7Cw==",
"license": "0BSD"
},
"node_modules/ical.js": {
"version": "1.5.0",
+2 -2
View File
@@ -34,8 +34,8 @@
"@types/cytoscape-klay": "^3.1.5",
"@types/js-cookie": "^3.0.6",
"@types/node": "^26.2.0",
"@typescript/native": "npm:typescript@^7.0.2",
"rollup-plugin-visualizer": "^7.1.1",
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.2.2"
},
@@ -61,7 +61,7 @@
"easymde": "^2.21.0",
"glob": "^13.0.6",
"html2canvas": "^1.4.1",
"htmx.org": "^4.0.0",
"htmx.org": "^2.0.10",
"js-cookie": "^3.0.8",
"lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1",
+88 -11
View File
@@ -64,23 +64,100 @@ $pedagogy-white-text: #f0f0f0;
}
#search_form {
.search-form-container {
display: grid;
grid-template-columns: auto auto;
grid-template-rows: auto auto auto;
grid-template-areas:
"action-bar action-bar"
"search-bar search-bar"
"radio-department radio-department"
"radio-credit-type radio-semester";
}
.radio-guide fieldset {
input[type="checkbox"] {
display: none;
.action-bar {
grid-area: action-bar;
margin-bottom: 10px;
}
.search-bar {
grid-area: search-bar;
display: grid;
grid-template-columns: auto 200px;
grid-template-rows: auto;
grid-template-areas: "search-bar-input search-bar-button";
@media screen and (max-width: $medium-devices) {
grid-template-columns: auto auto;
grid-template-rows: auto;
grid-template-areas: "search-bar-input search-bar-button";
}
@media screen and (max-width: $small-devices) {
grid-template-columns: auto;
grid-template-rows: auto;
grid-template-areas: "search-bar-input";
.search-bar-button {
display: none;
}
}
.search-bar-input {
grid-area: search-bar-input;
background: $pedagogy-light-blue;
}
.search-bar-button {
grid-area: search-bar-button;
background: $pedagogy-orange;
color: white;
font-weight: bold;
margin-left: 20px;
}
}
.radio-department {
grid-area: radio-department;
}
.radio-credit-type {
grid-area: radio-credit-type;
}
.radio-semester {
grid-area: radio-semester;
}
.radio-guide input[type="radio"],
input[type="checkbox"] {
display: none;
}
.radio-guide {
margin-top: 10px;
margin-bottom: 0;
color: white;
}
label {
padding: 10px 20px;
}
.radio-guide label {
display: inline-block;
background-color: $pedagogy-blue;
padding: 10px 20px;
font-family: Arial, sans-serif;
font-size: 16px;
border-radius: 4px;
}
input[type="checkbox"]:checked+label {
background-color: $pedagogy-orange;
@include shadow;
}
.radio-guide input[type="radio"]:checked+label {
background-color: $pedagogy-orange;
}
.radio-guide input[type="checkbox"]:checked+label {
background-color: $pedagogy-orange;
}
.radio-guide label:hover {
background-color: $pedagogy-hover-blue;
}
}
+29 -30
View File
@@ -14,7 +14,7 @@
{% endblock %}
{% block additional_js %}
<script type="module" src="{{ static('bundled/pedagogy/guide-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/pedagogy/guide-index.js') }}"></script>
{% endblock %}
{% block head %}
@@ -24,28 +24,30 @@
{% block content %}
{% if user.has_perm("pedagogy.add_ue") %}
<div class="row gap">
<a href="{{ url('pedagogy:ue_create') }}" class="btn btn-blue"><i class="fa fa-plus"></i>{% trans %}Create UE{% endtrans %}</a>
<a href="{{ url('pedagogy:moderation') }}" class="btn btn-grey">{% trans %}Moderate comments{% endtrans %}</a>
<div class="action-bar">
<p>
<a href="{{ url('pedagogy:ue_create') }}">{% trans %}Create UE{% endtrans %}</a>
</p>
<p>
<a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a>
</p>
</div>
<br/>
{% endif %}
<div class="pedagogy" x-data="ue_search" x-cloak>
<form id="search_form" class="">
<form id="search_form">
<div class="search-form-container">
<fieldset>
<label for="search_input">{% trans %}Search UE{% endtrans %}</label>
<div class="search-bar">
<input
id="search_input"
class="search-bar-input"
type="search"
type="text"
name="search"
placeholder=""
x-model.debounce.500ms="search"
/>
</fieldset>
<div class="row gap-3x margin-bottom radio-guide">
<fieldset>
</div>
<div class="radio-department">
<div class="radio-guide">
{% set departments = [
("EDIM", "EDIM"), ("ENERGIE", "EE"), ("IMSI", "IMSI"),
("INFO", "GI"), ("GMC", "MC"), ("HUMA", "HUMA"), ("TC", "TC")
@@ -54,16 +56,16 @@
<input
type="checkbox"
name="department"
id="radio_{{ real_name }}"
id="radio{{ real_name }}"
value="{{ real_name }}"
x-model="department"
/>
<label for="radio_{{ real_name }}" class="btn btn-blue">
{% trans %}{{ display_name }}{% endtrans %}
</label>
<label for="radio{{ real_name }}">{% trans %}{{ display_name }}{% endtrans %}</label>
{% endfor %}
</fieldset>
<fieldset>
</div>
</div>
<div class="radio-credit-type">
<div class="radio-guide">
{% for credit_type in ["CS", "TM", "EC", "QC", "OM"] %}
<input
type="checkbox"
@@ -72,21 +74,18 @@
value="{{ credit_type }}"
x-model="credit_type"
/>
<label for="radio{{ credit_type }}" class="btn btn-blue">
{% trans %}{{ credit_type }}{% endtrans %}
</label>
<label for="radio{{ credit_type }}">{% trans %}{{ credit_type }}{% endtrans %}</label>
{% endfor %}
</fieldset>
<fieldset>
</div>
</div>
<div class="radio-semester">
<div class="radio-guide">
<input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN" x-model="semester"/>
<label for="radioAUTUMN" class="btn btn-no-text btn-blue">
<i class="fa fa-leaf"></i>
</label>
<label for="radioAUTUMN"><i class="fa fa-leaf"></i></label>
<input type="checkbox" name="semester" id="radioSPRING" value="SPRING" x-model="semester"/>
<label for="radioSPRING" class="btn btn-no-text btn-blue">
<i class="fa-regular fa-sun"></i>
</label>
</fieldset>
<label for="radioSPRING"><i class="fa-regular fa-sun"></i></label>
</div>
</div>
</div>
</form>
@@ -1,47 +0,0 @@
# Generated by Django 5.2.17 on 2026-09-01 08:44
from typing import TYPE_CHECKING
import django.utils.timezone
from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import F, Min, Value
from django.db.models.functions import Least
from django.utils.timezone import now
if TYPE_CHECKING:
import subscription.models
def make_default_creation_date(apps: StateApps, schema_editor):
Subscription = apps.get_model("subscription", "Subscription")
Subscription.objects.update(
created_at=Least(
F("subscription_start"), Value(now()), output_field=models.DateTimeField()
)
)
class Migration(migrations.Migration):
dependencies = [("subscription", "0016_alter_subscription_subscription_type")]
operations = [
migrations.AddField(
model_name="subscription",
name="created_at",
field=models.DateTimeField(
auto_now_add=True,
default=django.utils.timezone.now,
help_text=(
"When this subscription was created. "
"This date may differ from the start of the subscription."
),
verbose_name="created_at",
),
preserve_default=False,
),
migrations.RunPython(
make_default_creation_date,
reverse_code=migrations.RunPython.noop,
elidable=True,
),
]
-8
View File
@@ -64,14 +64,6 @@ class Subscription(models.Model):
max_length=20,
verbose_name=_("location"),
)
created_at = models.DateTimeField(
_("created_at"),
help_text=_(
"When this subscription was created. "
"This date may differ from the start of the subscription."
),
auto_now_add=True,
)
class Meta:
ordering = ["subscription_start"]
@@ -1,7 +1,7 @@
<form
hx-post="{{ url("subscription:fragment-existing-user") }}"
hx-target="this"
hx-disable="find input[type='submit']"
hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML"
>
{% csrf_token %}
@@ -1,7 +1,7 @@
<form
hx-post="{{ url("subscription:fragment-new-user") }}"
hx-target="this"
hx-disable="find input[type='submit']"
hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML"
>
{% csrf_token %}