Compare commits

..

8 Commits

Author SHA1 Message Date
imperosol
710bf56c10 add tests 2026-04-17 22:52:00 +02:00
imperosol
0dcfb5e18b add translations 2026-04-17 22:52:00 +02:00
imperosol
9c924c5b14 ask for user confirmation if its role was moved out of presidency 2026-04-17 22:52:00 +02:00
imperosol
33902b4e15 add links to club edit page 2026-04-17 18:19:01 +02:00
imperosol
37a77f6cca add links to club edit page
add help texts in the club roles edition page
2026-04-17 18:19:01 +02:00
imperosol
bd67c370bc feat: page to create club roles 2026-04-17 18:19:01 +02:00
imperosol
ec5d22e59b feat: page to edit and reorder club role 2026-04-17 18:18:59 +02:00
imperosol
62bf882442 adapt tests to new club roles framework 2026-04-17 18:18:13 +02:00
15 changed files with 865 additions and 95 deletions

View File

@@ -330,3 +330,64 @@ class ClubSearchForm(forms.ModelForm):
# so we enforce it.
self.fields["club_status"].value = True
self.fields["name"].required = False
class ClubRoleForm(forms.ModelForm):
error_css_class = "error"
required_css_class = "required"
class Meta:
model = ClubRole
fields = ["name", "description", "is_presidency", "is_board", "is_active"]
widgets = {
"is_presidency": forms.HiddenInput(),
"is_board": forms.HiddenInput(),
"is_active": forms.CheckboxInput(attrs={"class": "switch"}),
}
def clean(self):
cleaned_data = super().clean()
if "ORDER" in cleaned_data:
self.instance.order = cleaned_data["ORDER"]
return cleaned_data
class ClubRoleCreateForm(forms.ModelForm):
"""Form to create a club role.
Notes:
For UX purposes, users are not meant to fill `is_presidency`
and `is_board`, so those values are required by the form constructor
in order to initialize the instance properly.
"""
error_css_class = "error"
required_css_class = "required"
class Meta:
model = ClubRole
fields = ["name", "description"]
def __init__(
self, *args, club: Club, is_presidency: bool, is_board: bool, **kwargs
):
super().__init__(*args, **kwargs)
self.instance.club = club
self.instance.is_presidency = is_presidency
self.instance.is_board = is_board
class ClubRoleBaseFormSet(forms.BaseInlineFormSet):
ordering_widget = forms.HiddenInput()
ClubRoleFormSet = forms.inlineformset_factory(
Club,
ClubRole,
ClubRoleForm,
ClubRoleBaseFormSet,
can_delete=False,
can_order=True,
edit_only=True,
extra=0,
)

View File

@@ -128,6 +128,9 @@ class Migration(migrations.Migration):
("is_presidency", False), ("is_board", True), _connector="OR"
),
name="clubrole_presidency_implies_board",
violation_error_message=(
"A role cannot be in the presidency while not being in the board"
),
),
),
migrations.RunPython(migrate_roles, migrations.RunPython.noop),

View File

@@ -204,6 +204,15 @@ class Club(models.Model):
"""Method to see if that object can be edited by the given user."""
return self.has_rights_in_club(user)
def can_roles_be_edited_by(self, user: User) -> bool:
"""Return True if the given user can edit the roles of this club"""
return (
user.has_perm("club.change_clubrole")
or self.members.ongoing()
.filter(user=user, role__is_presidency=True)
.exists()
)
@cached_property
def current_members(self) -> list[Membership]:
return list(
@@ -251,6 +260,9 @@ class ClubRole(OrderedModel):
models.CheckConstraint(
condition=Q(is_presidency=False) | Q(is_board=True),
name="clubrole_presidency_implies_board",
violation_error_message=_(
"A role cannot be in the presidency while not being in the board"
),
)
]
@@ -260,21 +272,8 @@ class ClubRole(OrderedModel):
def get_display_name(self):
return f"{self.name} - {self.club.name}"
def get_absolute_url(self):
return reverse("club:club_roles", kwargs={"club_id": self.club_id})
def clean(self):
errors = []
if self.is_presidency and not self.is_board:
errors.append(
ValidationError(
_(
"Role %(name)s was declared as a presidency role "
"without being a board role"
)
% {"name": self.name}
)
)
roles = list(self.club.roles.all())
if (
self.is_board

View File

@@ -0,0 +1,61 @@
import type { AlpineComponent } from "alpinejs";
interface RoleGroupData {
isBoard: boolean;
isPresidency: boolean;
roleId: number;
}
document.addEventListener("alpine:init", () => {
Alpine.data("clubRoleList", (config: { userRoleId: number | null }) => ({
confirmOnSubmit: false,
/**
* Edit relevant item data after it has been moved by x-sort
*/
reorder(item: AlpineComponent<RoleGroupData>, conf: RoleGroupData) {
item.isBoard = conf.isBoard;
item.isPresidency = conf.isPresidency;
// if the user has moved its own role outside the presidency,
// submitting the form will require a confirmation
this.confirmOnSubmit = config.userRoleId === item.roleId && !item.isPresidency;
this.resetOrder();
},
/**
* Reset the value of the ORDER input of all items in the list.
* This is to be called after any reordering operation, in order to make sure
* that the order that will be saved is coherent with what is displayed.
*/
resetOrder() {
// When moving items with x-sort, the only information we truly have is
// the end position in the target group, not the previous position nor
// the position in the global list.
// To overcome this, we loop through an enumeration of all inputs
// that are in the form `roles-X-ORDER` and sequentially set the value of the field.
const inputs = document.querySelectorAll<HTMLInputElement>(
"input[name^='roles'][name$='ORDER']",
);
for (const [i, elem] of inputs.entries()) {
elem.value = (i + 1).toString();
}
},
/**
* If the user moved its role out of the presidency, ask a confirmation
* before submitting the form
*/
confirmSubmission(event: SubmitEvent) {
if (
this.confirmOnSubmit &&
!confirm(
gettext(
"You're going to remove your own role from the presidency. " +
"You may lock yourself out of this page. Do you want to continue ? ",
),
)
) {
event.preventDefault();
}
},
}));
});

View File

@@ -0,0 +1,7 @@
.fa-grip-vertical {
display: flex;
flex-direction: column;
justify-content: center;
cursor: pointer;
margin-right: .5em;
}

View File

@@ -12,6 +12,15 @@
<h2>{% trans %}Club members{% endtrans %}</h2>
{% if club.can_roles_be_edited_by(user) %}
<a
href="{{ url("club:club_roles", club_id=object.id) }}"
class="btn btn-blue margin-bottom"
>
{% trans %}Manage roles{% endtrans %}
</a>
{% endif %}
{% if add_member_fragment %}
<br />
{{ add_member_fragment }}

View File

@@ -0,0 +1,159 @@
{% extends "core/base.jinja" %}
{% block additional_js %}
<script type="module" src="{{ static("bundled/club/role-list-index.ts") }}"></script>
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("club/roles.scss") }}">
{% endblock %}
{% macro display_subform(subform) %}
<div
class="row"
x-data="{
isPresidency: {{ subform.is_presidency.value()|lower }},
isBoard: {{ subform.is_board.value()|lower }},
roleId: {{ subform.id.value() }},
}"
x-sort:item="$data"
>
{# hidden fields #}
{{ subform.ORDER }}
{{ subform.id }}
{{ subform.club }}
{{ subform.is_presidency|add_attr("x-model=isPresidency") }}
{{ subform.is_board|add_attr("x-model=isBoard") }}
<i class="fa fa-grip-vertical" x-sort:handle></i>
<details class="accordion grow" {% if subform.errors %}open{% endif %}>
<summary>
{{ subform.name.value() }}
{% if not subform.instance.is_active -%}
({% trans %}inactive{% endtrans %})
{%- endif %}
</summary>
<div class="accordion-content">
{{ subform.non_field_errors() }}
<div class="form-group">
{{ subform.name.as_field_group() }}
</div>
<div class="form-group">
{{ subform.description.as_field_group() }}
</div>
<div class="form-group">
<div>
{{ subform.is_active }}
{{ subform.is_active.label_tag() }}
</div>
<span class="helptext">
{{ subform.is_active.help_text }}
</span>
</div>
</div>
</details>
</div>
{% endmacro %}
{% block content %}
<p>
{% trans trimmed %}
Roles give rights on the club.
Higher roles grant more rights, and the members having them are displayed higher
in the club members list.
{% endtrans %}
</p>
<p>
{% trans trimmed %}
On this page, you can edit their name and description, as well as their order.
You can also drag roles from a category to another
(e.g. a board role can be made into a presidency role).
{% endtrans %}
</p>
<form
method="post"
x-data="clubRoleList({ userRoleId: {{ user_role or "null" }} })"
@submit="confirmSubmission"
>
{% csrf_token %}
{{ form.management_form }}
{{ form.non_form_errors() }}
<h3>{% trans %}Presidency{% endtrans %}</h3>
<a class="btn btn-blue" href="{{ url("club:new_role_president", club_id=club.id) }}">
<i class="fa fa-plus"></i> {% trans %}add role{% endtrans %}
</a>
{# The style we use for markdown rendering is quite nice for what we want to display,
so we are just gonna reuse it. #}
<div class="markdown">
<p>{% trans %}Users with a presidency role can :{% endtrans %}</p>
<ul>
<li>{% trans %}create new club roles and edit existing ones{% endtrans %}</li>
<li>{% trans %}manage the club counters{% endtrans %}</li>
<li>{% trans %}add new members with any active role and end any membership{% endtrans %}</li>
</ul>
<p>{% trans %}They also have all the rights of the club board.{% endtrans %}</p>
</div>
<br/>
<div
x-sort="reorder($item, { isBoard: true, isPresidency: true })"
x-sort:group="roles"
>
{% for subform in form %}
{% if subform.is_presidency.value() %}
{{ display_subform(subform) }}
{% endif %}
{% endfor %}
</div>
<h3>{% trans %}Board{% endtrans %}</h3>
<a class="btn btn-blue" href="{{ url("club:new_role_board", club_id=club.id) }}">
<i class="fa fa-plus"></i> {% trans %}add role{% endtrans %}
</a>
<div class="markdown">
<p>
{% trans trimmed %}
Board members can do most administrative actions in the club, including :
{% endtrans %}
</p>
<ul>
<li>{% trans %}manage the club posters{% endtrans %}</li>
<li>{% trans %}create news for the club{% endtrans %}</li>
<li>{% trans %}click users on the club's counters{% endtrans %}</li>
<li>
{% trans trimmed %}
add new members and end active memberships
for roles that are lower than their own.
{% endtrans %}
</li>
</ul>
</div>
<br/>
<div
x-sort="reorder($item, { isBoard: true, isPresidency: false })"
x-sort:group="roles"
>
{% for subform in form %}
{% if subform.is_board.value() and not subform.is_presidency.value() %}
{{ display_subform(subform) }}
{% endif %}
{% endfor %}
</div>
<h3>{% trans %}Members{% endtrans %}</h3>
<a class="btn btn-blue" href="{{ url("club:new_role_member", club_id=club.id) }}">
<i class="fa fa-plus"></i> {% trans %}add role{% endtrans %}
</a>
<div class="markdown">
<p>{% trans %}Simple members cannot perform administrative actions.{% endtrans %}</p>
</div>
<br/>
<div
x-sort="reorder($item, { isBoard: false, isPresidency: false })"
x-sort:group="roles"
>
{% for subform in form %}
{% if not subform.is_board.value() %}
{{ display_subform(subform) }}
{% endif %}
{% endfor %}
</div>
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form>
{% endblock content %}

View File

@@ -5,8 +5,19 @@
<div>
<h4>{% trans %}Communication:{% endtrans %}</h4>
<ul>
<li> <a href="{{ url('com:news_new') }}?club={{ object.id }}">{% trans %}Create a news{% endtrans %}</a></li>
<li> <a href="{{ url('com:weekmail_article') }}?club={{ object.id }}">{% trans %}Post in the Weekmail{% endtrans %}</a></li>
<li>
<a href="{{ url('com:news_new') }}?club={{ object.id }}">
{% trans %}Create a news{% endtrans %}
</a>
</li>
<li>
<a href="{{ url('com:weekmail_article') }}?club={{ object.id }}">
{% trans %}Post in the Weekmail{% endtrans %}
</a>
</li>
{% if object.can_roles_be_edited_by(user) %}
<li><a href="{{ url("club:club_roles", club_id=object.id) }}"></a></li>
{% endif %}
{% if object.trombi %}
<li> <a href="{{ url('trombi:detail', trombi_id=object.trombi.id) }}">{% trans %}Edit Trombi{% endtrans %}</a></li>
{% else %}

View File

@@ -1,25 +1,48 @@
from collections.abc import Callable
import pytest
from django.contrib.auth.models import Permission
from django.test import Client, TestCase
from django.urls import reverse
from model_bakery import baker, seq
from model_bakery.recipe import Recipe
from pytest_django.asserts import assertRedirects
from club.models import Club, ClubRole
from club.forms import ClubRoleFormSet
from club.models import Club, ClubRole, Membership
from core.baker_recipes import subscriber_user
from core.models import User
@pytest.mark.django_db
def test_order_auto():
"""Test that newly created roles are put in the right place."""
def make_club():
# unittest-style tests cannot use fixture, so we create a function
# that will be callable either by a pytest fixture or inside
# a TestCase.setUpTestData method.
club = baker.make(Club)
recipe = Recipe(ClubRole, club=club, name=seq("role "))
# bulk create initial roles (1 presidency, 1 board, 1 member)
roles = recipe.make(
recipe.make(
is_board=iter([True, True, False]),
is_presidency=iter([True, False, False]),
order=iter([1, 2, 3]),
_quantity=3,
_bulk_create=True,
)
# then create the remaining roles one by one (like they will be in prod)
return club
@pytest.fixture
def club(db):
"""A club with a presidency role, a board role and a member role"""
return make_club()
@pytest.mark.django_db
def test_order_auto(club):
"""Test that newly created roles are put in the right place."""
roles = list(club.roles.all())
# create new roles one by one (like they will be in prod)
# each new role should be placed at the end of its category
recipe = Recipe(ClubRole, club=club, name=seq("new role "))
role_a = recipe.make(is_board=True, is_presidency=True, order=None)
role_b = recipe.make(is_board=True, is_presidency=False, order=None)
role_c = recipe.make(is_board=False, is_presidency=False, order=None)
@@ -31,3 +54,183 @@ def test_order_auto():
roles[2],
role_c,
]
@pytest.mark.django_db
@pytest.mark.parametrize(
("user_factory", "is_allowed"),
[
(
lambda club: baker.make(
User,
user_permissions=[Permission.objects.get(codename="change_clubrole")],
),
True,
),
( # user with presidency roles can edit the club roles
lambda club: subscriber_user.make(
memberships=[
baker.make(
Membership,
club=club,
role=club.roles.filter(is_presidency=True).first(),
)
]
),
True,
),
( # user in the board but not in the presidency cannot edit roles
lambda club: subscriber_user.make(
memberships=[
baker.make(
Membership,
club=club,
role=club.roles.filter(
is_presidency=False, is_board=True
).first(),
)
]
),
False,
),
],
)
def test_can_roles_be_edited_by(
club: Club, user_factory: Callable[[Club], User], is_allowed
):
"""Test that `Club.can_roles_be_edited_by` return the right value"""
user = user_factory(club)
assert club.can_roles_be_edited_by(user) == is_allowed
@pytest.mark.django_db
@pytest.mark.parametrize(
["route", "is_presidency", "is_board"],
[
("club:new_role_president", True, True),
("club:new_role_board", False, True),
("club:new_role_member", False, False),
],
)
def test_create_role_view(client: Client, route: str, is_presidency, is_board):
"""Test that the role creation views work."""
club = baker.make(Club)
role = baker.make(ClubRole, club=club, is_presidency=True, is_board=True)
user = subscriber_user.make()
baker.make(Membership, club=club, role=role, user=user, end_date=None)
url = reverse(route, kwargs={"club_id": club.id})
client.force_login(user)
res = client.get(url)
assert res.status_code == 200
res = client.post(url, data={"name": "foo"})
assertRedirects(res, reverse("club:club_roles", kwargs={"club_id": club.id}))
new_role = club.roles.last()
assert new_role.name == "foo"
assert new_role.is_presidency == is_presidency
assert new_role.is_board == is_board
class TestClubRoleUpdate(TestCase):
@classmethod
def setUpTestData(cls):
cls.club = make_club()
cls.roles = list(cls.club.roles.all())
cls.user = subscriber_user.make()
baker.make(
Membership, club=cls.club, role=cls.roles[0], user=cls.user, end_date=None
)
cls.url = reverse("club:club_roles", kwargs={"club_id": cls.club.id})
def setUp(self):
self.payload = {
"roles-TOTAL_FORMS": 3,
"roles-INITIAL_FORMS": 3,
"roles-MIN_NUM_FORMS": 0,
"roles-MAX_NUM_FORMS": 1000,
"roles-0-ORDER": self.roles[0].order,
"roles-0-id": self.roles[0].id,
"roles-0-club": self.club.id,
"roles-0-is_presidency": True,
"roles-0-is_board": True,
"roles-0-name": self.roles[0].name,
"roles-0-description": self.roles[0].description,
"roles-0-is_active": True,
"roles-1-ORDER": self.roles[1].order,
"roles-1-id": self.roles[1].id,
"roles-1-club": self.club.id,
"roles-1-is_presidency": False,
"roles-1-is_board": True,
"roles-1-name": self.roles[1].name,
"roles-1-description": self.roles[1].description,
"roles-1-is_active": True,
"roles-2-ORDER": self.roles[2].order,
"roles-2-id": self.roles[2].id,
"roles-2-club": self.club.id,
"roles-2-is_presidency": False,
"roles-2-is_board": False,
"roles-2-name": self.roles[2].name,
"roles-2-description": self.roles[2].description,
"roles-2-is_active": True,
}
def test_view_ok(self):
"""Basic test to check that the view works."""
self.client.force_login(self.user)
res = self.client.get(self.url)
assert res.status_code == 200
self.payload["roles-2-name"] = "foo"
res = self.client.post(self.url, data=self.payload)
assertRedirects(res, self.url)
self.roles[2].refresh_from_db()
assert self.roles[2].name == "foo"
def test_incoherent_order(self):
"""Test that placing a member role over a board role fails."""
self.payload["roles-0-ORDER"] = 4
formset = ClubRoleFormSet(data=self.payload, instance=self.club)
assert not formset.is_valid()
assert formset.errors == [
{
"__all__": [
f"Le rôle {self.roles[0].name} ne peut pas "
"être placé en-dessous d'un rôle de membre.",
f"Le rôle {self.roles[0].name} ne peut pas être placé "
"en-dessous d'un rôle qui n'est pas de la présidence.",
]
},
{},
{},
]
def test_change_order_ok(self):
"""Test that changing order the intended way works"""
self.payload["roles-1-ORDER"] = 3
self.payload["roles-1-is_board"] = False
self.payload["roles-2-ORDER"] = 2
formset = ClubRoleFormSet(data=self.payload, instance=self.club)
assert formset.is_valid()
formset.save()
assert list(self.club.roles.order_by("order")) == [
self.roles[0],
self.roles[2],
self.roles[1],
]
self.roles[1].refresh_from_db()
assert not self.roles[1].is_board
def test_non_board_presidency_is_forbidden(self):
"""Test that a role cannot be in the presidency without being in the board."""
self.payload["roles-0-is_board"] = False
formset = ClubRoleFormSet(data=self.payload, instance=self.club)
assert not formset.is_valid()
assert formset.errors == [
{
"__all__": [
"Un rôle ne peut pas appartenir à la présidence sans être dans le bureau",
]
},
{},
{},
]

View File

@@ -35,6 +35,10 @@ from club.views import (
ClubPageEditView,
ClubPageHistView,
ClubRevView,
ClubRoleBoardCreateView,
ClubRoleMemberCreateView,
ClubRolePresidencyCreateView,
ClubRoleUpdateView,
ClubSellingCSVView,
ClubSellingView,
ClubToolsView,
@@ -71,6 +75,22 @@ urlpatterns = [
ClubOldMembersView.as_view(),
name="club_old_members",
),
path("<int:club_id>/role/", ClubRoleUpdateView.as_view(), name="club_roles"),
path(
"<int:club_id>/role/new/president/",
ClubRolePresidencyCreateView.as_view(),
name="new_role_president",
),
path(
"<int:club_id>/role/new/board/",
ClubRoleBoardCreateView.as_view(),
name="new_role_board",
),
path(
"<int:club_id>/role/new/member/",
ClubRoleMemberCreateView.as_view(),
name="new_role_member",
),
path("<int:club_id>/sellings/", ClubSellingView.as_view(), name="club_sellings"),
path(
"<int:club_id>/sellings/csv/", ClubSellingCSVView.as_view(), name="sellings_csv"

View File

@@ -28,7 +28,11 @@ import csv
import itertools
from typing import TYPE_CHECKING, Any
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.mixins import (
LoginRequiredMixin,
PermissionRequiredMixin,
UserPassesTestMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
from django.core.paginator import InvalidPage, Paginator
@@ -55,12 +59,14 @@ from club.forms import (
ClubAdminEditForm,
ClubEditForm,
ClubOldMemberForm,
ClubRoleCreateForm,
ClubRoleFormSet,
ClubSearchForm,
JoinClubForm,
MailingForm,
SellingsForm,
)
from club.models import Club, Mailing, MailingSubscription, Membership
from club.models import Club, ClubRole, Mailing, MailingSubscription, Membership
from com.models import Poster
from com.views import (
PosterCreateBaseView,
@@ -414,6 +420,118 @@ class ClubOldMembersView(ClubTabsMixin, PermissionRequiredMixin, DetailView):
}
class ClubRoleUpdateView(
ClubTabsMixin, UserPassesTestMixin, SuccessMessageMixin, UpdateView
):
form_class = ClubRoleFormSet
model = Club
template_name = "club/club_roles.jinja"
pk_url_kwarg = "club_id"
current_tab = "members"
success_message = _("Club roles updated")
def test_func(self):
return (
self.request.user.is_authenticated
and self.get_object().can_roles_be_edited_by(self.request.user)
)
def get_form_kwargs(self):
return super().get_form_kwargs() | {"form_kwargs": {"label_suffix": ""}}
def get_success_url(self):
return self.request.path
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"user_role": ClubRole.objects.filter(
club=self.object,
members__user=self.request.user,
members__end_date=None,
)
.values_list("id", flat=True)
.first()
}
class ClubRoleBaseCreateView(UserPassesTestMixin, SuccessMessageMixin, CreateView):
"""View to create a new Club Role, using [][club.forms.ClubRoleCreateForm].
This view isn't meant to be called directly, but rather subclassed for each
type of role that can exist :
- `[ClubRolePresidencyCreateView][club.views.ClubRolePresidencyCreateView]`
to create a presidency role
- `[ClubRoleBoardCreateView][club.views.ClubRoleBoardCreateView]`
to create a board role
- `[ClubRoleMemberCreateView][club.views.ClubRoleMemberCreateView]`
to create a member role
Each subclass have to override the following variables :
- `is_presidency` and `is_board`, indicating what type of role
the view creates.
- `role_description`, which is the title of the page, indication
the user what kind of role is being created.
This way, we are making sure the correct type of role will
be created, without bothering the user with the implementation details.
"""
form_class = ClubRoleCreateForm
model = ClubRole
template_name = "core/create.jinja"
success_message = _("Role %(name)s created")
role_description = ""
is_presidency: bool
is_board: bool
@cached_property
def club(self):
return get_object_or_404(Club, id=self.kwargs["club_id"])
def test_func(self):
return self.request.user.is_authenticated and (
self.request.user.has_perm("club.add_clubrole")
or self.club.members.filter(
user=self.request.user, role__is_presidency=True
).exists()
)
def get_form_kwargs(self):
return super().get_form_kwargs() | {
"club": self.club,
"is_presidency": self.is_presidency,
"is_board": self.is_board,
}
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"object_name": self.role_description
}
def get_success_url(self):
return reverse("club:club_roles", kwargs={"club_id": self.club.id})
class ClubRolePresidencyCreateView(ClubRoleBaseCreateView):
is_presidency = True
is_board = True
role_description = _("club role \u2013 presidency")
class ClubRoleBoardCreateView(ClubRoleBaseCreateView):
is_presidency = False
is_board = True
role_description = _("club role \u2013 board")
class ClubRoleMemberCreateView(ClubRoleBaseCreateView):
is_presidency = False
is_board = False
role_description = _("club role \u2013 member")
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
"""Sales of a club."""

View File

@@ -53,7 +53,7 @@ details.accordion>.accordion-content {
opacity: 0;
@supports (max-height: calc-size(max-content, size)) {
max-height: 0px;
max-height: 0;
}
}
@@ -71,11 +71,12 @@ details.accordion>.accordion-content {
}
}
// ::details-content isn't available on firefox yet
// ::details-content is available on firefox only since september 2025
// (and wasn't available when this code was initially written)
// we use .accordion-content as a workaround
// But we need to use ::details-content for chrome because it's
// not working correctly otherwise
// it only happen in chrome, not safari or firefox
// it only happens in chrome, not safari or firefox
// Note: `selector` is not supported by scss so we comment it out to
// avoid compiling it and sending it straight to the css
// This is a trick that comes from here :

View File

@@ -1,11 +1,18 @@
{% extends "core/base.jinja" %}
{# if the template context has the `object_name` variable,
then this one will be used in the page title,
instead of the result of `str(object)` #}
{% if not object_name %}
{% set object_name=form.instance.__class__._meta.verbose_name %}
{% endif %}
{% block title %}
{% trans name=form.instance.__class__._meta.verbose_name %}Create {{ name }}{% endtrans %}
{% trans name=object_name %}Create {{ name }}{% endtrans %}
{% endblock %}
{% block content %}
<h2>{% trans name=form.instance.__class__._meta.verbose_name %}Create {{ name }}{% endtrans %}</h2>
<h2>{% trans name=object_name %}Create {{ name }}{% endtrans %}</h2>
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p() }}

View File

@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-16 19:05+0200\n"
"POT-Creation-Date: 2026-04-17 22:42+0200\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -181,6 +181,22 @@ msgstr "Vous devez être cotisant pour faire partie d'un club"
msgid "You are already a member of this club"
msgstr "Vous êtes déjà membre de ce club."
#: club/forms.py
msgid "Club status"
msgstr "État du club"
#: club/forms.py
msgid "Active"
msgstr "Actif"
#: club/forms.py
msgid "Inactive"
msgstr "Inactif"
#: club/forms.py
msgid "All clubs"
msgstr "Tous les clubs"
#: club/models.py
msgid "slug name"
msgstr "nom slug"
@@ -254,26 +270,22 @@ msgid "club roles"
msgstr "rôles de club"
#: club/models.py
#, python-format
msgid ""
"Role %(name)s was declared as a presidency role without being a board role"
msgid "A role cannot be in the presidency while not being in the board"
msgstr ""
"Le rôle %(name)s a été déclaré comme rôle de présidence sans être un rôle du "
"bureau."
"Un rôle ne peut pas appartenir à la présidence sans être dans le bureau"
#: club/models.py
#, python-format
msgid "Role %(role)s cannot be placed below a member role"
msgstr ""
"Le rôle %(role)s ne peut pas être placé en-dessous d'un rôle de "
"membre."
"Le rôle %(role)s ne peut pas être placé en-dessous d'un rôle de membre."
#: club/models.py
#, python-format
msgid "Role %(role)s cannot be placed below a non-presidency role"
msgstr ""
"Le rôle %(role)s ne peut pas être placé en-dessous d'un rôle de "
"membre."
"Le rôle %(role)s ne peut pas être placé en-dessous d'un rôle qui n'est pas "
"de la présidence."
#: club/models.py core/models.py counter/models.py eboutic/models.py
#: election/models.py pedagogy/models.py sas/models.py trombi/models.py
@@ -350,43 +362,28 @@ msgstr "Cet email est déjà abonné à cette mailing"
msgid "Unregistered user"
msgstr "Utilisateur non enregistré"
#: club/templates/club/club_list.jinja
msgid "Club list"
msgstr "Liste des clubs"
#: club/templates/club/club_list.jinja
msgid "The list of all clubs existing at UTBM."
msgstr "La liste de tous les clubs existants à l'UTBM"
#: club/templates/club/club_list.jinja
msgid "Club list"
msgstr "Liste des clubs"
#: club/templates/club/club_list.jinja
msgid "Filters"
msgstr "Filtres"
#: club/templates/club/club_list.jinja
msgid "Name"
msgstr "Nom"
#: club/templates/club/club_list.jinja
msgid "Club state"
msgstr "Etat du club"
#: club/templates/club/club_list.jinja
msgid "Active"
msgstr "Actif"
#: club/templates/club/club_list.jinja
msgid "Inactive"
msgstr "Inactif"
#: club/templates/club/club_list.jinja
msgid "All clubs"
msgstr "Tous les clubs"
#: club/templates/club/club_list.jinja core/templates/core/base/header.jinja
#: forum/templates/forum/macros.jinja matmat/templates/matmat/search_form.jinja
msgid "Search"
msgstr "Recherche"
#: club/templates/club/club_list.jinja core/templates/core/user_tools.jinja
msgid "New club"
msgstr "Nouveau club"
#: club/templates/club/club_list.jinja
#: club/templates/club/club_list.jinja club/templates/club/club_roles.jinja
msgid "inactive"
msgstr "inactif"
@@ -394,6 +391,10 @@ msgstr "inactif"
msgid "Club members"
msgstr "Membres du club"
#: club/templates/club/club_members.jinja
msgid "Manage roles"
msgstr "Gérer les rôles"
#: club/templates/club/club_members.jinja
#: club/templates/club/club_old_members.jinja
#: core/templates/core/user_clubs.jinja
@@ -432,6 +433,116 @@ msgstr "Du"
msgid "To"
msgstr "Au"
#: club/templates/club/club_roles.jinja
msgid ""
"Roles give rights on the club. Higher roles grant more rights, and the "
"members having them are displayed higher in the club members list."
msgstr ""
"Les rôles donnent des droits sur le club. Les rôles plus élevés donnent plus "
"de droit, et les membres qui les possèdent sont affichés plus haut dans la "
"liste des membres."
#: club/templates/club/club_roles.jinja
msgid ""
"On this page, you can edit their name and description, as well as their "
"order. You can also drag roles from a category to another (e.g. a board role "
"can be made into a presidency role)."
msgstr ""
"Sur cette page, vous pouvez éditer leur nom et leur description, ainsi que "
"leur ordre. Vous pouvez également déplacer des rôles d'une catégorie à "
"l'autre (par exemple, un rôle du bureau peut devenir un rôle de présidence)."
#: club/templates/club/club_roles.jinja
msgid "Presidency"
msgstr "Présidence"
#: club/templates/club/club_roles.jinja
msgid "add role"
msgstr "ajouter un rôle"
#: club/templates/club/club_roles.jinja
msgid "Users with a presidency role can :"
msgstr "Les utilisateurs avec un rôle de présidence peuvent :"
#: club/templates/club/club_roles.jinja
msgid "create new club roles and edit existing ones"
msgstr "créer de nouveaux rôles et modifier ceux qui existent"
#: club/templates/club/club_roles.jinja
msgid "manage the club counters"
msgstr "gérer les comptoirs du club"
#: club/templates/club/club_roles.jinja
msgid "add new members with any active role and end any membership"
msgstr ""
"ajouter de nouveaux membres avec n'importe quel rôle et mettre fin à "
"n'importe quelle adhésion au club."
#: club/templates/club/club_roles.jinja
msgid "They also have all the rights of the club board."
msgstr "Ils possèdent également tous les droits du bureau."
#: club/templates/club/club_roles.jinja
msgid "Board"
msgstr "Bureau"
#: club/templates/club/club_roles.jinja
msgid ""
"Board members can do most administrative actions in the club, including :"
msgstr ""
"Les membres du bureau peuvent effectuer la plupart des actions "
"administratives dans le club, incluant :"
#: club/templates/club/club_roles.jinja
msgid "manage the club posters"
msgstr "gérer les affiches du club"
#: club/templates/club/club_roles.jinja
msgid "create news for the club"
msgstr "créer des nouvelles pour le club"
#: club/templates/club/club_roles.jinja
msgid "click users on the club's counters"
msgstr "cliquer des utilisateurs sur les comptoirs du club"
#: club/templates/club/club_roles.jinja
msgid ""
"add new members and end active memberships for roles that are lower than "
"their own."
msgstr ""
"ajouter de nouveaux membres et mettre fin à des adhésions en cours, pour des "
"rôles plus bas que le leur."
#: club/templates/club/club_roles.jinja club/views.py
msgid "Members"
msgstr "Membres"
#: club/templates/club/club_roles.jinja
msgid "Simple members cannot perform administrative actions."
msgstr ""
"Les simples membres ne peuvent pas effectuer d'actions administratives."
#: club/templates/club/club_roles.jinja club/templates/club/edit_club.jinja
#: club/templates/club/pagerev_edit.jinja com/templates/com/news_edit.jinja
#: com/templates/com/poster_edit.jinja com/templates/com/screen_edit.jinja
#: com/templates/com/weekmail.jinja core/templates/core/create.jinja
#: core/templates/core/edit.jinja core/templates/core/file_edit.jinja
#: core/templates/core/fragment/user_visibility.jinja
#: core/templates/core/page/edit.jinja core/templates/core/page/prop.jinja
#: core/templates/core/user_godfathers.jinja
#: core/templates/core/user_godfathers_tree.jinja
#: core/templates/core/user_preferences.jinja
#: counter/templates/counter/cash_register_summary.jinja
#: counter/templates/counter/invoices_call.jinja
#: counter/templates/counter/product_form.jinja
#: forum/templates/forum/reply.jinja
#: subscription/templates/subscription/fragments/creation_form.jinja
#: trombi/templates/trombi/comment.jinja
#: trombi/templates/trombi/edit_profile.jinja
#: trombi/templates/trombi/user_tools.jinja
msgid "Save"
msgstr "Sauver"
#: club/templates/club/club_sellings.jinja
msgid "Previous"
msgstr "Précédent"
@@ -616,27 +727,6 @@ msgstr ""
"Les champs de formulaire suivants sont liées à la description basique d'un "
"club. Tous les membres du bureau du club peuvent voir et modifier ceux-ci."
#: club/templates/club/edit_club.jinja club/templates/club/pagerev_edit.jinja
#: com/templates/com/news_edit.jinja com/templates/com/poster_edit.jinja
#: com/templates/com/screen_edit.jinja com/templates/com/weekmail.jinja
#: core/templates/core/create.jinja core/templates/core/edit.jinja
#: core/templates/core/file_edit.jinja
#: core/templates/core/fragment/user_visibility.jinja
#: core/templates/core/page/edit.jinja core/templates/core/page/prop.jinja
#: core/templates/core/user_godfathers.jinja
#: core/templates/core/user_godfathers_tree.jinja
#: core/templates/core/user_preferences.jinja
#: counter/templates/counter/cash_register_summary.jinja
#: counter/templates/counter/invoices_call.jinja
#: counter/templates/counter/product_form.jinja
#: forum/templates/forum/reply.jinja
#: subscription/templates/subscription/fragments/creation_form.jinja
#: trombi/templates/trombi/comment.jinja
#: trombi/templates/trombi/edit_profile.jinja
#: trombi/templates/trombi/user_tools.jinja
msgid "Save"
msgstr "Sauver"
#: club/templates/club/fragments/add_member.jinja
msgid "Add a new member"
msgstr "Ajouter un nouveau membre"
@@ -717,10 +807,6 @@ msgstr "Éditer la page"
msgid "Infos"
msgstr "Infos"
#: club/views.py
msgid "Members"
msgstr "Membres"
#: club/views.py
msgid "Old members"
msgstr "Anciens membres"
@@ -770,6 +856,27 @@ msgstr "Vous êtes maintenant membre de ce club."
msgid "%(user)s has been added to club."
msgstr "%(user)s a été ajouté au club."
#: club/views.py
msgid "Club roles updated"
msgstr "Rôles de club mis à jour"
#: club/views.py
#, python-format
msgid "Role %(name)s created"
msgstr "Rôle %(name)s créé"
#: club/views.py
msgid "club role presidency"
msgstr "rôle de club présidence"
#: club/views.py
msgid "club role board"
msgstr "rôle de club bureau"
#: club/views.py
msgid "club role member"
msgstr "rôle de club membre"
#: club/views.py
msgid "Benefit"
msgstr "Bénéfice"
@@ -1912,11 +2019,6 @@ msgstr "Connexion"
msgid "Register"
msgstr "Inscription"
#: core/templates/core/base/header.jinja forum/templates/forum/macros.jinja
#: matmat/templates/matmat/search_form.jinja
msgid "Search"
msgstr "Recherche"
#: core/templates/core/base/header.jinja
msgid "Logout"
msgstr "Déconnexion"

View File

@@ -7,7 +7,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-26 15:45+0100\n"
"POT-Creation-Date: 2026-04-17 22:42+0200\n"
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -17,6 +17,14 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: club/static/bundled/club/role-list-index.ts
msgid ""
"You're going to remove your own role from the presidency. You may lock "
"yourself out of this page. Do you want to continue ? "
msgstr ""
"Vous vous apprêtez à retirer votre propre rôle de la présidence. Vous risquez "
"de perdre l'accès à cette page. Voulez-vous continuer ?"
#: com/static/bundled/com/components/ics-calendar-index.ts
msgid "More info"
msgstr "Plus d'informations"
@@ -271,4 +279,5 @@ msgstr "Il n'a pas été possible de supprimer l'image"
msgid ""
"Wrong timetable format. Make sure you copied if from your student folder."
msgstr ""
"Mauvais format d'emploi du temps. Assurez-vous que vous l'avez copié depuis votre dossier étudiants."
"Mauvais format d'emploi du temps. Assurez-vous que vous l'avez copié depuis "
"votre dossier étudiant."