mirror of
https://github.com/ae-utbm/sith.git
synced 2026-04-18 01:08:23 +00:00
Compare commits
8 Commits
club-role
...
club-role-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
710bf56c10 | ||
|
|
0dcfb5e18b | ||
|
|
9c924c5b14 | ||
|
|
33902b4e15 | ||
|
|
37a77f6cca | ||
|
|
bd67c370bc | ||
|
|
ec5d22e59b | ||
|
|
62bf882442 |
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
61
club/static/bundled/club/role-list-index.ts
Normal file
61
club/static/bundled/club/role-list-index.ts
Normal 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();
|
||||
}
|
||||
},
|
||||
}));
|
||||
});
|
||||
7
club/static/club/roles.scss
Normal file
7
club/static/club/roles.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.fa-grip-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
margin-right: .5em;
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
159
club/templates/club/club_roles.jinja
Normal file
159
club/templates/club/club_roles.jinja
Normal 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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
},
|
||||
{},
|
||||
{},
|
||||
]
|
||||
|
||||
20
club/urls.py
20
club/urls.py
@@ -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"
|
||||
|
||||
122
club/views.py
122
club/views.py
@@ -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."""
|
||||
|
||||
|
||||
@@ -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 :
|
||||
|
||||
@@ -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() }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user