Compare commits

...
Author SHA1 Message Date
imperosol d21c90dc72 add translations 2026-09-01 18:39:55 +02:00
imperosol d2f9398937 Show linked groups on club role form page 2026-09-01 18:39:55 +02:00
imperosol 8153d7a105 Allow club role-group m2m links 2026-09-01 18:32:37 +02:00
thomas girodandGitHub 7fca5d8c75 Merge pull request #1468 from ae-utbm/fix-duplicate-user-search
fix: duplicate user search when a whitelist exists
2026-08-31 18:03:19 +02:00
imperosol 3467aad846 fix: duplicate user search when a whitelist exists
Quand un utilisateur possède une whitelist d'utilisateurs, qu'il effectue une recherche et qu'il apparait dans les résultats, son profil apparait plusieurs fois.
2026-08-31 16:52:42 +02:00
klmp200andGitHub ebfac638de Merge pull request #1465 from ae-utbm/hotfix
Fix broken product widget
2026-08-30 22:40:08 +02:00
klmp200andGitHub 839536661b Merge pull request #1466 from ae-utbm/htmx4
Migrate to HTMX4
2026-08-30 22:39:52 +02:00
thomas girodandGitHub b1b3639dc9 Merge pull request #1467 from ae-utbm/pedagogy-style
Pedagogy style
2026-08-30 22:38:34 +02:00
klmp200 56c832ba06 Migrate to HTMX4 2026-08-30 22:07:53 +02:00
imperosol 884019d803 style: standardize UE search style 2026-08-30 21:53:00 +02:00
klmp200 dfc714ff3d Fix broken ProductAjaxSelect widget 2026-08-28 15:05:17 +02:00
22 changed files with 244 additions and 155 deletions
+2 -2
View File
@@ -46,8 +46,8 @@ class ClubAdmin(admin.ModelAdmin):
@admin.register(ClubRole) @admin.register(ClubRole)
class ClubRoleAdmin(admin.ModelAdmin): class ClubRoleAdmin(admin.ModelAdmin):
list_display = ("name", "club", "is_board", "is_presidency") list_display = ("name", "club", "is_board", "is_presidency")
search_fields = ("name",) search_fields = ("name", "club__name")
autocomplete_fields = ("club",) autocomplete_fields = ("club", "linked_groups")
list_select_related = ("club",) list_select_related = ("club",)
list_filter = ( list_filter = (
"is_board", "is_board",
+7
View File
@@ -479,6 +479,13 @@ class ClubRoleCreateForm(forms.ModelForm):
class ClubRoleBaseFormSet(forms.BaseInlineFormSet): class ClubRoleBaseFormSet(forms.BaseInlineFormSet):
ordering_widget = forms.HiddenInput() ordering_widget = forms.HiddenInput()
def __init__(self, *args, queryset=None, **kwargs):
if queryset is None:
queryset = self.model._default_manager
super().__init__(
*args, queryset=queryset.prefetch_related("linked_groups"), **kwargs
)
ClubRoleFormSet = forms.inlineformset_factory( ClubRoleFormSet = forms.inlineformset_factory(
Club, Club,
@@ -0,0 +1,26 @@
# Generated by Django 5.2.17 on 2026-09-01 14:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("club", "0017_linktype_clublink"),
("core", "0050_alter_sithfile_moderator"),
]
operations = [
migrations.AddField(
model_name="clubrole",
name="linked_groups",
field=models.ManyToManyField(
help_text=(
"Groups that are automatically given or removed "
"to user receiving or losing this club role"
),
related_name="club_roles",
to="core.group",
verbose_name="Linked groups",
),
),
]
+33 -12
View File
@@ -23,6 +23,8 @@
# #
from __future__ import annotations from __future__ import annotations
import operator
from functools import reduce
from typing import Iterable, Self from typing import Iterable, Self
from django.conf import settings from django.conf import settings
@@ -282,6 +284,15 @@ class ClubRole(OrderedModel):
"If the role is inactive, people joining the club won't be able to get it." "If the role is inactive, people joining the club won't be able to get it."
), ),
) )
linked_groups = models.ManyToManyField(
Group,
verbose_name=_("Linked groups"),
help_text=_(
"Groups that are automatically given or removed "
"to user receiving or losing this club role"
),
related_name="linked_roles",
)
order_with_respect_to = "club" order_with_respect_to = "club"
@@ -534,7 +545,7 @@ class Membership(models.Model):
def _remove_club_groups( def _remove_club_groups(
memberships: Iterable[Membership], memberships: Iterable[Membership],
) -> tuple[int, dict[str, int]]: ) -> tuple[int, dict[str, int]]:
"""Remove users of those memberships from the club groups. """Remove users of those memberships from the club and club role groups.
For example, if a user is in the Troll club board, For example, if a user is in the Troll club board,
he is in the board group and the members group of the Troll. he is in the board group and the members group of the Troll.
@@ -553,15 +564,19 @@ class Membership(models.Model):
clubs = {m.club_id for m in memberships} clubs = {m.club_id for m in memberships}
users = {m.user_id for m in memberships} users = {m.user_id for m in memberships}
groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs)) groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs))
role_groups = [
Q(user_id=m.user_id, group__linked_roles=m.role_id) for m in memberships
]
return User.groups.through.objects.filter( return User.groups.through.objects.filter(
Q(group__in=groups) & Q(user__in=users) (Q(group__in=groups) & Q(user__in=users))
| reduce(operator.or_, role_groups)
).delete() ).delete()
@staticmethod @staticmethod
def _add_club_groups( def _add_club_groups(
memberships: Iterable[Membership], memberships: Iterable[Membership],
) -> list[User.groups.through]: ) -> list[User.groups.through]:
"""Add users of those memberships to the club groups. """Add users of those memberships to the club and club role groups.
For example, if a user just joined the Troll club board, For example, if a user just joined the Troll club board,
he will be added in both the members group and the board group he will be added in both the members group and the board group
@@ -582,34 +597,40 @@ class Membership(models.Model):
memberships = [m for m in memberships if m.end_date is None] memberships = [m for m in memberships if m.end_date is None]
if not memberships: if not memberships:
return [] return []
nb_prefetched = sum(
if sum(1 for m in memberships if not hasattr(m, "club")) > 1: 1 for m in memberships if not hasattr(m, "club") or not hasattr(m, "role")
)
if nb_prefetched > 1:
# if more than one membership hasn't its `club` attribute set # if more than one membership hasn't its `club` attribute set
# it's less expensive to reload the whole query with # it's less expensive to reload the whole query with
# a select_related than perform a distinct query # a select_related than perform a distinct query
# to fetch each club. # to fetch each club.
ids = {m.id for m in memberships} ids = {m.id for m in memberships}
memberships = list( memberships = list(
Membership.objects.filter(id__in=ids).select_related("club") Membership.objects.filter(id__in=ids)
.select_related("club", "role")
.prefetch_related("role__linked_groups")
) )
club_groups = [] groups = []
for membership in memberships: for membership in memberships:
club_groups.append( groups.append(
User.groups.through( User.groups.through(
user_id=membership.user_id, user_id=membership.user_id,
group_id=membership.club.members_group_id, group_id=membership.club.members_group_id,
) )
) )
if membership.role.is_board: if membership.role.is_board:
club_groups.append( groups.append(
User.groups.through( User.groups.through(
user_id=membership.user_id, user_id=membership.user_id,
group_id=membership.club.board_group_id, group_id=membership.club.board_group_id,
) )
) )
return User.groups.through.objects.bulk_create( groups.extend(
club_groups, ignore_conflicts=True User.groups.through(user_id=membership.user_id, group_id=g.id)
) for g in membership.role.linked_groups.all()
)
return User.groups.through.objects.bulk_create(groups, ignore_conflicts=True)
class Mailing(models.Model): class Mailing(models.Model):
+14
View File
@@ -49,6 +49,20 @@
{{ subform.is_active.help_text }} {{ subform.is_active.help_text }}
</span> </span>
</div> </div>
{% set groups = subform.instance.linked_groups.all()|list %}
{% if groups %}
<div>
<p>
<strong>{% trans %}Linked groups : {% endtrans %}</strong>
{{ groups|map(attribute="name")|join(", ") }}
</p>
<p class="helptext">
{% trans trimmed %}
Users receiving this role will also be assigned to those groups
{% endtrans %}
</p>
</div>
{% endif %}
</div> </div>
</details> </details>
</div> </div>
@@ -7,7 +7,7 @@
<form <form
hx-post="{{ url('club:club_new_members', club_id=club.id) }}" hx-post="{{ url('club:club_new_members', club_id=club.id) }}"
hx-disabled-elt="find input[type='submit']" hx-disable="find input[type='submit']"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-target="#member-fragment-container" hx-target="#member-fragment-container"
id="add_club_members_form" id="add_club_members_form"
+24 -1
View File
@@ -17,7 +17,7 @@ from club.forms import ClubAddMemberForm, JoinClubForm
from club.models import Club, ClubRole, Membership from club.models import Club, ClubRole, Membership
from club.tests.base import TestClub from club.tests.base import TestClub
from core.baker_recipes import subscriber_user from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, User from core.models import AnonymousUser, Group, User
class TestMembershipQuerySet(TestClub): class TestMembershipQuerySet(TestClub):
@@ -500,6 +500,29 @@ class TestMembership(TestClub):
assert self.subscriber.groups.contains(self.club.members_group) assert self.subscriber.groups.contains(self.club.members_group)
assert self.subscriber.groups.contains(self.club.board_group) assert self.subscriber.groups.contains(self.club.board_group)
def test_add_to_club_role_group(self):
groups = baker.make(Group, _quantity=4)
self.subscriber.groups.set(groups[:1])
self.board_role.linked_groups.set(groups[1:3])
baker.make(
Membership, club=self.club, user=self.subscriber, role=self.board_role
)
assert set(self.subscriber.groups.all()) == {
*groups[:3],
self.club.board_group,
self.club.members_group,
}
def test_remove_from_club_role_group(self):
groups = baker.make(Group, _quantity=3)
baker.make(
Membership, club=self.club, user=self.subscriber, role=self.board_role
)
self.subscriber.groups.set(groups[:1])
self.board_role.linked_groups.set(groups[1:])
self.subscriber.memberships.update(end_date=localdate())
assert set(self.subscriber.groups.all()) == {groups[0]}
def test_change_position_in_club(self): def test_change_position_in_club(self):
"""Test that when moving from board to members, club group change""" """Test that when moving from board to members, club group change"""
membership = baker.make( membership = baker.make(
+10 -5
View File
@@ -6,10 +6,15 @@
* for more efficient tree-shaking and gzip compression. * 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 sort from "@alpinejs/sort";
import Alpine from "alpinejs"; import Alpine from "alpinejs";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import htmx from "htmx.org";
import { limitedChoices } from "#core:alpine/limited-choices"; import { limitedChoices } from "#core:alpine/limited-choices";
import { expireOldStorage } from "#core:core/localstorage"; import { expireOldStorage } from "#core:core/localstorage";
import { default as navbar } from "#core:core/navbar"; import { default as navbar } from "#core:core/navbar";
@@ -44,16 +49,16 @@ polyfillCountryFlagEmojis();
* HTMX * HTMX
*/ */
document.body.addEventListener( document.body.addEventListener(
"htmx:beforeRequest" as keyof HTMLElementEventMap, "htmx:before:request" as keyof HTMLElementEventMap,
(event) => { (event) => {
(event as CustomEvent).detail.target.ariaBusy = true; (event as CustomEvent).detail.ctx.target.ariaBusy = true;
}, },
); );
document.body.addEventListener( document.body.addEventListener(
"htmx:beforeSwap" as keyof HTMLElementEventMap, "htmx:before:swap" as keyof HTMLElementEventMap,
(event) => { (event) => {
(event as CustomEvent).detail.target.ariaBusy = null; (event as CustomEvent).detail.ctx.target.ariaBusy = null;
}, },
); );
@@ -1,6 +1,6 @@
<form <form
hx-post="{{ url("core:user_visibility_fragment", user_id=form.instance.id) }}" hx-post="{{ url("core:user_visibility_fragment", user_id=form.instance.id) }}"
hx-disabled-elt="find input[type='submit']" hx-disable="find input[type='submit']"
hx-swap="outerHTML" x-data="{ isViewable: {{ form.is_viewable.value()|tojson }} }" hx-swap="outerHTML" x-data="{ isViewable: {{ form.is_viewable.value()|tojson }} }"
> >
{% for message in messages %} {% for message in messages %}
+16
View File
@@ -141,6 +141,22 @@ class TestSearchUsersView(TestSearchUsers):
response = self.client.get(reverse("core:search")) response = self.client.get(reverse("core:search"))
assert response.status_code == 200 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 @pytest.mark.django_db
def test_user_account_not_found(client: Client): def test_user_account_not_found(client: Client):
+1
View File
@@ -65,6 +65,7 @@ class SearchView(LoginRequiredMixin, TemplateView):
UserFilterSchema(search=query) UserFilterSchema(search=query)
.filter(User.objects.viewable_by(self.request.user)) .filter(User.objects.viewable_by(self.request.user))
.order_by(F("last_login").desc(nulls_last=True)) .order_by(F("last_login").desc(nulls_last=True))
.distinct()
) )
clubs = list(Club.objects.filter(name__icontains=query)[:5]) clubs = list(Club.objects.filter(name__icontains=query)[:5])
return super().get_context_data(**kwargs) | {"users": users, "clubs": clubs} return super().get_context_data(**kwargs) | {"users": users, "clubs": clubs}
@@ -28,18 +28,23 @@ export class ProductAjaxSelect extends AjaxSelect {
return []; return [];
} }
private getName(item: SimpleProductSchema, sanitize: typeof escape_html): string { // 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 {
return item.code ? `${sanitize(item.code)} - ${sanitize(item.name)}` : item.name; return item.code ? `${sanitize(item.code)} - ${sanitize(item.name)}` : item.name;
} }
protected renderOption(item: SimpleProductSchema, sanitize: typeof escape_html) { protected renderOption(item: SimpleProductSchema, sanitize: typeof escape_html) {
return `<div class="select-item"> return `<div class="select-item">
<span class="select-item-text">${this.getName(item, sanitize)}</span> <span class="select-item-text">${ProductAjaxSelect.getName(item, sanitize)}</span>
</div>`; </div>`;
} }
protected renderItem(item: SimpleProductSchema, sanitize: typeof escape_html) { protected renderItem(item: SimpleProductSchema, sanitize: typeof escape_html) {
return `<span>${this.getName(item, sanitize)}</span>`; return `<span>${ProductAjaxSelect.getName(item, sanitize)}</span>`;
} }
} }
@@ -1,5 +1,10 @@
<div id="student_card_form"> <div id="student_card_form">
<form hx-post="{{ action }}" hx-swap="outerHTML" hx-target="#student_card_form"> <form
hx-post="{{ action }}"
hx-swap="outerHTML"
hx-target="#student_card_form"
hx-disable="input[type='submit']"
>
{% csrf_token %} {% csrf_token %}
<p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p> <p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p>
<input type="submit" value="{% trans %}Confirm{% endtrans %}" /> <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 ### Htmx
[Site officiel](https://htmx.org/) [Site officiel](https://four.htmx.org/)
En plus de AlpineJS, linteractivité sur le site est augmentée via Htmx. 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 à 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-post="{{ url("election:apply_result", election_id=form.election.id) }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-target="#apply-election-result-fragment" hx-target="#apply-election-result-fragment"
hx-disabled-elt="find input[type='submit']" hx-disable="find input[type='submit']"
> >
{% csrf_token %} {% csrf_token %}
{{ form }} {{ form }}
+37 -1
View File
@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-21 14:10+0200\n" "POT-Creation-Date: 2026-09-01 18:25+0200\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -260,6 +260,18 @@ msgstr ""
"Si ce rôle est inactif, il ne pourra pas être attribué aux gens qui " "Si ce rôle est inactif, il ne pourra pas être attribué aux gens qui "
"rejoignent le club." "rejoignent le club."
#: club/models.py
msgid "Linked groups"
msgstr "Groupes liés"
#: club/models.py
msgid ""
"Groups that are automatically given or removed to user receiving or losing "
"this club role"
msgstr ""
"Les groupes qui sont automatiquement donnés ou retirés quand l'utilisateur "
"reçoit ou perd ce rôle de club"
#: club/models.py election/models.py #: club/models.py election/models.py
msgid "club role" msgid "club role"
msgstr "rôle de club" msgstr "rôle de club"
@@ -485,6 +497,14 @@ msgstr "Du"
msgid "To" msgid "To"
msgstr "Au" msgstr "Au"
#: club/templates/club/club_roles.jinja
msgid "Linked groups : "
msgstr "Groupes liés : "
#: club/templates/club/club_roles.jinja
msgid "Users receiving this role will also be assigned to those groups"
msgstr "Les utilisateurs recevant ce rôle seront aussi assignés à ces groupes"
#: club/templates/club/club_roles.jinja #: club/templates/club/club_roles.jinja
msgid "" msgid ""
"Roles give rights on the club. Higher roles grant more rights, and the " "Roles give rights on the club. Higher roles grant more rights, and the "
@@ -5089,6 +5109,10 @@ msgstr "signalant"
msgid "A guide of courses available at UTBM." msgid "A guide of courses available at UTBM."
msgstr "Un guide de tous les cours disponibles à l'UTBM." msgstr "Un guide de tous les cours disponibles à l'UTBM."
#: pedagogy/templates/pedagogy/guide.jinja
msgid "Search UE"
msgstr "Recherche d'UE"
#: pedagogy/templates/pedagogy/guide.jinja #: pedagogy/templates/pedagogy/guide.jinja
#, python-format #, python-format
msgid "%(display_name)s" msgid "%(display_name)s"
@@ -5758,6 +5782,18 @@ msgstr "fin de la cotisation"
msgid "location" msgid "location"
msgstr "lieu" msgstr "lieu"
#: subscription/models.py
msgid "created_at"
msgstr "créé le"
#: subscription/models.py
msgid ""
"When this subscription was created. This date may differ from the start of "
"the subscription."
msgstr ""
"Quand la cotisation a été créée. Cette date peut différer du début effectif de "
"la cotisation."
#: subscription/models.py #: subscription/models.py
msgid "You can not subscribe many time for the same period" msgid "You can not subscribe many time for the same period"
msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période" msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période"
+11 -5
View File
@@ -30,7 +30,7 @@
"easymde": "^2.21.0", "easymde": "^2.21.0",
"glob": "^13.0.6", "glob": "^13.0.6",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"htmx.org": "^2.0.10", "htmx.org": "^4.0.0",
"js-cookie": "^3.0.8", "js-cookie": "^3.0.8",
"lit-html": "^3.3.3", "lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1", "native-file-system-adapter": "^3.0.1",
@@ -3801,10 +3801,16 @@
} }
}, },
"node_modules/htmx.org": { "node_modules/htmx.org": {
"version": "2.0.10", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz", "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-4.0.0.tgz",
"integrity": "sha512-kdeJe7ZVwaS6QMz/ebBIVtZdpwen6L0OQ5GOhPV9MKBb196TCZeZu4yA7ZIQsaLKv7EpXz+So7KSXNuHXhj7Cw==", "integrity": "sha512-T/171FUY93Kdfp8t+DnHdk45QvKRiBhVhhrwSzrXgUi4pHKvhp77dUA/qg8FAjsFWPIHNbmUuIdCrcVHuiZWng==",
"license": "0BSD" "license": "BSD-0-Clause",
"workspaces": [
"ext/*"
],
"bin": {
"upgrade-check": "dist/scripts/upgrade-check.js"
}
}, },
"node_modules/ical.js": { "node_modules/ical.js": {
"version": "1.5.0", "version": "1.5.0",
+2 -2
View File
@@ -34,8 +34,8 @@
"@types/cytoscape-klay": "^3.1.5", "@types/cytoscape-klay": "^3.1.5",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^26.2.0", "@types/node": "^26.2.0",
"rollup-plugin-visualizer": "^7.1.1",
"@typescript/native": "npm:typescript@^7.0.2", "@typescript/native": "npm:typescript@^7.0.2",
"rollup-plugin-visualizer": "^7.1.1",
"typescript": "npm:@typescript/typescript6@^6.0.2", "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.2.2" "vite": "^8.2.2"
}, },
@@ -61,7 +61,7 @@
"easymde": "^2.21.0", "easymde": "^2.21.0",
"glob": "^13.0.6", "glob": "^13.0.6",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"htmx.org": "^2.0.10", "htmx.org": "^4.0.0",
"js-cookie": "^3.0.8", "js-cookie": "^3.0.8",
"lit-html": "^3.3.3", "lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1", "native-file-system-adapter": "^3.0.1",
+11 -88
View File
@@ -64,100 +64,23 @@ $pedagogy-white-text: #f0f0f0;
} }
#search_form { #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";
}
.action-bar { .radio-guide fieldset {
grid-area: action-bar; input[type="checkbox"] {
margin-bottom: 10px; display: none;
}
.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-top: 10px;
margin-bottom: 0;
color: white; color: white;
}
.radio-guide label { label {
display: inline-block; padding: 10px 20px;
background-color: $pedagogy-blue; }
padding: 10px 20px;
font-family: Arial, sans-serif;
font-size: 16px;
border-radius: 4px;
}
.radio-guide input[type="radio"]:checked+label { input[type="checkbox"]:checked+label {
background-color: $pedagogy-orange; background-color: $pedagogy-orange;
} @include shadow;
}
.radio-guide input[type="checkbox"]:checked+label {
background-color: $pedagogy-orange;
}
.radio-guide label:hover {
background-color: $pedagogy-hover-blue;
} }
} }
+30 -29
View File
@@ -14,7 +14,7 @@
{% endblock %} {% endblock %}
{% block additional_js %} {% block additional_js %}
<script type="module" src="{{ static('bundled/pedagogy/guide-index.js') }}"></script> <script type="module" src="{{ static('bundled/pedagogy/guide-index.ts') }}"></script>
{% endblock %} {% endblock %}
{% block head %} {% block head %}
@@ -24,30 +24,28 @@
{% block content %} {% block content %}
{% if user.has_perm("pedagogy.add_ue") %} {% if user.has_perm("pedagogy.add_ue") %}
<div class="action-bar"> <div class="row gap">
<p> <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:ue_create') }}">{% trans %}Create UE{% endtrans %}</a> <a href="{{ url('pedagogy:moderation') }}" class="btn btn-grey">{% trans %}Moderate comments{% endtrans %}</a>
</p>
<p>
<a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a>
</p>
</div> </div>
<br/> <br/>
{% endif %} {% endif %}
<div class="pedagogy" x-data="ue_search" x-cloak> <div class="pedagogy" x-data="ue_search" x-cloak>
<form id="search_form"> <form id="search_form" class="">
<div class="search-form-container"> <div class="search-form-container">
<div class="search-bar"> <fieldset>
<label for="search_input">{% trans %}Search UE{% endtrans %}</label>
<input <input
id="search_input" id="search_input"
class="search-bar-input" class="search-bar-input"
type="text" type="search"
name="search" name="search"
placeholder=""
x-model.debounce.500ms="search" x-model.debounce.500ms="search"
/> />
</div> </fieldset>
<div class="radio-department"> <div class="row gap-3x margin-bottom radio-guide">
<div class="radio-guide"> <fieldset>
{% set departments = [ {% set departments = [
("EDIM", "EDIM"), ("ENERGIE", "EE"), ("IMSI", "IMSI"), ("EDIM", "EDIM"), ("ENERGIE", "EE"), ("IMSI", "IMSI"),
("INFO", "GI"), ("GMC", "MC"), ("HUMA", "HUMA"), ("TC", "TC") ("INFO", "GI"), ("GMC", "MC"), ("HUMA", "HUMA"), ("TC", "TC")
@@ -56,16 +54,16 @@
<input <input
type="checkbox" type="checkbox"
name="department" name="department"
id="radio{{ real_name }}" id="radio_{{ real_name }}"
value="{{ real_name }}" value="{{ real_name }}"
x-model="department" x-model="department"
/> />
<label for="radio{{ real_name }}">{% trans %}{{ display_name }}{% endtrans %}</label> <label for="radio_{{ real_name }}" class="btn btn-blue">
{% trans %}{{ display_name }}{% endtrans %}
</label>
{% endfor %} {% endfor %}
</div> </fieldset>
</div> <fieldset>
<div class="radio-credit-type">
<div class="radio-guide">
{% for credit_type in ["CS", "TM", "EC", "QC", "OM"] %} {% for credit_type in ["CS", "TM", "EC", "QC", "OM"] %}
<input <input
type="checkbox" type="checkbox"
@@ -74,18 +72,21 @@
value="{{ credit_type }}" value="{{ credit_type }}"
x-model="credit_type" x-model="credit_type"
/> />
<label for="radio{{ credit_type }}">{% trans %}{{ credit_type }}{% endtrans %}</label> <label for="radio{{ credit_type }}" class="btn btn-blue">
{% trans %}{{ credit_type }}{% endtrans %}
</label>
{% endfor %} {% endfor %}
</div> </fieldset>
</div> <fieldset>
<div class="radio-semester">
<div class="radio-guide">
<input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN" x-model="semester"/> <input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN" x-model="semester"/>
<label for="radioAUTUMN"><i class="fa fa-leaf"></i></label> <label for="radioAUTUMN" class="btn btn-no-text btn-blue">
<i class="fa fa-leaf"></i>
</label>
<input type="checkbox" name="semester" id="radioSPRING" value="SPRING" x-model="semester"/> <input type="checkbox" name="semester" id="radioSPRING" value="SPRING" x-model="semester"/>
<label for="radioSPRING"><i class="fa-regular fa-sun"></i></label> <label for="radioSPRING" class="btn btn-no-text btn-blue">
</div> <i class="fa-regular fa-sun"></i>
</label>
</fieldset>
</div> </div>
</div> </div>
</form> </form>
@@ -1,7 +1,7 @@
<form <form
hx-post="{{ url("subscription:fragment-existing-user") }}" hx-post="{{ url("subscription:fragment-existing-user") }}"
hx-target="this" hx-target="this"
hx-disabled-elt="find input[type='submit']" hx-disable="find input[type='submit']"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
{% csrf_token %} {% csrf_token %}
@@ -1,7 +1,7 @@
<form <form
hx-post="{{ url("subscription:fragment-new-user") }}" hx-post="{{ url("subscription:fragment-new-user") }}"
hx-target="this" hx-target="this"
hx-disabled-elt="find input[type='submit']" hx-disable="find input[type='submit']"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
{% csrf_token %} {% csrf_token %}