mirror of
https://github.com/ae-utbm/sith.git
synced 2026-05-14 04:58:06 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec0426a095 | |||
| 7d76bf77c6 | |||
| a672c1ebbe | |||
| 0c2abc9d4b | |||
| d974ae2e09 | |||
| f285fd23d0 | |||
| c45aa82305 | |||
| dc41379139 | |||
| 667768fb66 | |||
| d859e4fe5a | |||
| 045ad590eb |
+1
-16
@@ -16,7 +16,7 @@ from django.contrib import admin
|
|||||||
from django.forms.models import ModelForm
|
from django.forms.models import ModelForm
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from club.models import Club, ClubLink, ClubRole, LinkType, Membership
|
from club.models import Club, ClubRole, Membership
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Club)
|
@admin.register(Club)
|
||||||
@@ -67,18 +67,3 @@ class MembershipAdmin(admin.ModelAdmin):
|
|||||||
"club__name",
|
"club__name",
|
||||||
)
|
)
|
||||||
autocomplete_fields = ("user",)
|
autocomplete_fields = ("user",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(LinkType)
|
|
||||||
class LinkTypeAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ("name", "url_base", "icon")
|
|
||||||
search_fields = ("name",)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ClubLink)
|
|
||||||
class ClubLinkAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ("link_type", "club", "url")
|
|
||||||
list_select_related = ("link_type", "club")
|
|
||||||
autocomplete_fields = ("link_type", "club")
|
|
||||||
search_fields = ("link_type__name", "url")
|
|
||||||
list_filter = ("link_type", ("club", admin.RelatedOnlyFieldListFilter))
|
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ class ClubController(ControllerBase):
|
|||||||
queryset=Membership.objects.ongoing().select_related("user", "role"),
|
queryset=Membership.objects.ongoing().select_related("user", "role"),
|
||||||
)
|
)
|
||||||
return self.get_object_or_exception(
|
return self.get_object_or_exception(
|
||||||
Club.objects.prefetch_related(prefetch, "links"), id=club_id
|
Club.objects.prefetch_related(prefetch), id=club_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+62
-42
@@ -28,14 +28,7 @@ from django.db.models.functions import Lower
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from club.models import (
|
from club.models import Club, ClubRole, Mailing, MailingSubscription, Membership
|
||||||
Club,
|
|
||||||
ClubLink,
|
|
||||||
ClubRole,
|
|
||||||
Mailing,
|
|
||||||
MailingSubscription,
|
|
||||||
Membership,
|
|
||||||
)
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from core.views.forms import SelectDateTime
|
from core.views.forms import SelectDateTime
|
||||||
from core.views.widgets.ajax_select import (
|
from core.views.widgets.ajax_select import (
|
||||||
@@ -46,26 +39,6 @@ from counter.models import Counter, Selling
|
|||||||
from counter.schemas import SaleFilterSchema
|
from counter.schemas import SaleFilterSchema
|
||||||
|
|
||||||
|
|
||||||
class ClubLinkForm(forms.ModelForm):
|
|
||||||
error_css_class = "error"
|
|
||||||
required_css_class = "required"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ClubLink
|
|
||||||
fields = ["url", "name", "link_type"]
|
|
||||||
widgets = {
|
|
||||||
"url": forms.URLInput(
|
|
||||||
{"pattern": "https://.*", "placeholder": "https://monlien.com"}
|
|
||||||
),
|
|
||||||
"link_type": forms.HiddenInput(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ClubLinkFormSet = forms.inlineformset_factory(
|
|
||||||
Club, ClubLink, ClubLinkForm, extra=0, can_delete_extra=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ClubEditForm(forms.ModelForm):
|
class ClubEditForm(forms.ModelForm):
|
||||||
error_css_class = "error"
|
error_css_class = "error"
|
||||||
required_css_class = "required"
|
required_css_class = "required"
|
||||||
@@ -75,20 +48,6 @@ class ClubEditForm(forms.ModelForm):
|
|||||||
fields = ["address", "logo", "short_description"]
|
fields = ["address", "logo", "short_description"]
|
||||||
widgets = {"short_description": forms.Textarea()}
|
widgets = {"short_description": forms.Textarea()}
|
||||||
|
|
||||||
def __init__(self, *args, prefix: str | None = None, instance=None, **kwargs):
|
|
||||||
super().__init__(*args, prefix=prefix, instance=instance, **kwargs)
|
|
||||||
self.link_formset = ClubLinkFormSet(
|
|
||||||
*args, instance=self.instance, prefix="link", **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
return super().is_valid() and self.link_formset.is_valid()
|
|
||||||
|
|
||||||
def save(self, commit=True): # noqa: FBT002
|
|
||||||
res = super().save(commit=commit)
|
|
||||||
self.link_formset.save(commit=commit)
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
class ClubAdminEditForm(ClubEditForm):
|
class ClubAdminEditForm(ClubEditForm):
|
||||||
admin_fields = ["name", "parent", "is_active"]
|
admin_fields = ["name", "parent", "is_active"]
|
||||||
@@ -371,3 +330,64 @@ class ClubSearchForm(forms.ModelForm):
|
|||||||
# so we enforce it.
|
# so we enforce it.
|
||||||
self.fields["club_status"].value = True
|
self.fields["club_status"].value = True
|
||||||
self.fields["name"].required = False
|
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"] - 1
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -148,6 +148,9 @@ class Migration(migrations.Migration):
|
|||||||
("is_presidency", False), ("is_board", True), _connector="OR"
|
("is_presidency", False), ("is_board", True), _connector="OR"
|
||||||
),
|
),
|
||||||
name="clubrole_presidency_implies_board",
|
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),
|
migrations.RunPython(migrate_roles, migrations.RunPython.noop),
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
# Generated by Django 5.2.12 on 2026-04-27 07:39
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [("club", "0016_clubrole_alter_membership_role")]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="LinkType",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"name",
|
|
||||||
models.CharField(max_length=40, unique=True, verbose_name="name"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"url_base",
|
|
||||||
models.URLField(
|
|
||||||
help_text=(
|
|
||||||
"L'url de base que tous les "
|
|
||||||
"liens de ce type doivent respecter "
|
|
||||||
"(par exemple `https://www.instagram.com`)"
|
|
||||||
),
|
|
||||||
unique=True,
|
|
||||||
verbose_name="url base",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"icon",
|
|
||||||
models.CharField(
|
|
||||||
help_text=(
|
|
||||||
"The fontawesome class to use "
|
|
||||||
"(e.g. `fa-brands fa-instagram`)"
|
|
||||||
),
|
|
||||||
max_length=40,
|
|
||||||
verbose_name="icon",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={"verbose_name": "link type", "verbose_name_plural": "link types"},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="ClubLink",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"name",
|
|
||||||
models.CharField(blank=True, max_length=40, verbose_name="name"),
|
|
||||||
),
|
|
||||||
("url", models.URLField(verbose_name="link url")),
|
|
||||||
(
|
|
||||||
"created_at",
|
|
||||||
models.DateTimeField(auto_now_add=True, verbose_name="created at"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"updated_at",
|
|
||||||
models.DateTimeField(auto_now=True, verbose_name="updated at"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"club",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="links",
|
|
||||||
to="club.club",
|
|
||||||
verbose_name="club",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"link_type",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="links",
|
|
||||||
to="club.linktype",
|
|
||||||
verbose_name="link type",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={"verbose_name": "club link", "verbose_name_plural": "club links"},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
+12
-84
@@ -238,6 +238,15 @@ class Club(models.Model):
|
|||||||
"""Method to see if that object can be edited by the given user."""
|
"""Method to see if that object can be edited by the given user."""
|
||||||
return self.has_rights_in_club(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.is_authenticated and (
|
||||||
|
user.has_perm("club.change_clubrole")
|
||||||
|
or self.members.ongoing()
|
||||||
|
.filter(user=user, role__is_presidency=True)
|
||||||
|
.exists()
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def current_members(self) -> list[Membership]:
|
def current_members(self) -> list[Membership]:
|
||||||
return list(
|
return list(
|
||||||
@@ -285,6 +294,9 @@ class ClubRole(OrderedModel):
|
|||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
condition=Q(is_presidency=False) | Q(is_board=True),
|
condition=Q(is_presidency=False) | Q(is_board=True),
|
||||||
name="clubrole_presidency_implies_board",
|
name="clubrole_presidency_implies_board",
|
||||||
|
violation_error_message=_(
|
||||||
|
"A role cannot be in the presidency while not being in the board"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -294,21 +306,8 @@ class ClubRole(OrderedModel):
|
|||||||
def get_display_name(self):
|
def get_display_name(self):
|
||||||
return f"{self.name} - {self.club.name}"
|
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):
|
def clean(self):
|
||||||
errors = []
|
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())
|
roles = list(self.club.roles.all())
|
||||||
if (
|
if (
|
||||||
self.is_board
|
self.is_board
|
||||||
@@ -774,74 +773,3 @@ class MailingSubscription(models.Model):
|
|||||||
|
|
||||||
def fetch_format(self):
|
def fetch_format(self):
|
||||||
return self.get_email + " "
|
return self.get_email + " "
|
||||||
|
|
||||||
|
|
||||||
class LinkType(models.Model):
|
|
||||||
"""A link type, in order to group links and give them icons.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
Among all club links, there is a special one, with an empty base url
|
|
||||||
and a default link icon.
|
|
||||||
It is use as a fallback item when no actual link type can be found.
|
|
||||||
|
|
||||||
Danger:
|
|
||||||
LinkType.icon is content that will be raw-rendered in the template.
|
|
||||||
It is NOT safe to allow users to give it.
|
|
||||||
The edition of this field must be reserved to trusted admins.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=40, unique=True)
|
|
||||||
url_base = models.URLField(
|
|
||||||
"url base",
|
|
||||||
unique=True,
|
|
||||||
help_text=_(
|
|
||||||
"The base url that links with this type must respect (e.g. `%(url)s`)"
|
|
||||||
)
|
|
||||||
% {"url": "https://www.instagram.com"},
|
|
||||||
)
|
|
||||||
icon = models.CharField(
|
|
||||||
_("icon"),
|
|
||||||
max_length=40,
|
|
||||||
help_text=_("The fontawesome class to use (e.g. `fa-brands fa-instagram`)"),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("link type")
|
|
||||||
verbose_name_plural = _("link types")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class ClubLink(models.Model):
|
|
||||||
link_type = models.ForeignKey(
|
|
||||||
LinkType,
|
|
||||||
verbose_name=_("link type"),
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="links",
|
|
||||||
)
|
|
||||||
name = models.CharField(_("name"), max_length=40, blank=True)
|
|
||||||
url = models.URLField(_("link url"))
|
|
||||||
club = models.ForeignKey(
|
|
||||||
Club, verbose_name=_("club"), on_delete=models.CASCADE, related_name="links"
|
|
||||||
)
|
|
||||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(_("updated at"), auto_now=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("club link")
|
|
||||||
verbose_name_plural = _("club links")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.url
|
|
||||||
|
|
||||||
def save(self, **kwargs):
|
|
||||||
if not self.name:
|
|
||||||
self.name = self.link_type.name
|
|
||||||
return super().save(**kwargs)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if not self.url.startswith(self.link_type.url_base):
|
|
||||||
raise ValidationError(
|
|
||||||
_("This link doesn't match with the url base of its type.")
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from typing import Annotated
|
|||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from ninja import FilterLookup, FilterSchema, ModelSchema
|
from ninja import FilterLookup, FilterSchema, ModelSchema
|
||||||
from pydantic import HttpUrl
|
|
||||||
|
|
||||||
from club.models import Club, ClubRole, Membership
|
from club.models import Club, ClubRole, Membership
|
||||||
from core.schemas import NonEmptyStr, SimpleUserSchema
|
from core.schemas import NonEmptyStr, SimpleUserSchema
|
||||||
@@ -63,11 +62,6 @@ class ClubSchema(ModelSchema):
|
|||||||
fields = ["id", "name", "logo", "is_active", "short_description", "address"]
|
fields = ["id", "name", "logo", "is_active", "short_description", "address"]
|
||||||
|
|
||||||
members: list[ClubMemberSchema]
|
members: list[ClubMemberSchema]
|
||||||
links: list[HttpUrl]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_links(obj: Club):
|
|
||||||
return [link.url for link in obj.links.all()]
|
|
||||||
|
|
||||||
|
|
||||||
class UserMembershipSchema(ModelSchema):
|
class UserMembershipSchema(ModelSchema):
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#club-detail {
|
|
||||||
img.club-logo {
|
|
||||||
display: block;
|
|
||||||
max-height: 200px;
|
|
||||||
max-width: 200px;
|
|
||||||
}
|
|
||||||
#club-attributes {
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
margin-left: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: .75rem;
|
|
||||||
|
|
||||||
li i {
|
|
||||||
margin-right: .5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.has-links) {
|
|
||||||
#club-attributes {
|
|
||||||
float: right;
|
|
||||||
margin: 1em 0 1em 2em;
|
|
||||||
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 400px) {
|
|
||||||
float: unset;
|
|
||||||
img.club-logo {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.has-links {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
gap: 2em;
|
|
||||||
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#club-attributes {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1em;
|
|
||||||
min-width: 200px;
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
margin-top: 1em;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
justify-content: flex-end;
|
|
||||||
h4 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
img.club-logo {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.fa-grip-vertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
@@ -21,43 +21,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block additional_css %}
|
|
||||||
<link rel="stylesheet" href="{{ static("club/detail.scss") }}">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{{ club.name }}</h3>
|
<div id="club_detail">
|
||||||
<div id="club-detail" {% if links %}class="has-links"{% endif %}>
|
{% if club.logo %}
|
||||||
<div id="club-attributes">
|
<div class="club_logo"><img src="{{ club.logo.url }}" alt="{{ club.name }}"></div>
|
||||||
{% if club.logo %}
|
{% endif %}
|
||||||
<img
|
<h3>{{ club.name }}</h3>
|
||||||
class="club-logo"
|
{% if page_revision %}
|
||||||
src="{{ club.logo.url }}"
|
{{ page_revision|markdown }}
|
||||||
alt="{{ club.name }}"
|
{% endif %}
|
||||||
width="{{ club.logo.width }}"
|
|
||||||
height="{{ club.logo.height }}"
|
|
||||||
>
|
|
||||||
{% endif %}
|
|
||||||
{% if links %}
|
|
||||||
<div id="club-links">
|
|
||||||
<h4>{% trans %}Links{% endtrans %}</h4>
|
|
||||||
<ul>
|
|
||||||
{% for link in links %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ link.url }}" rel="noopener" target="_blank">
|
|
||||||
<i class="{{ link.link_type.icon }} fa-xl"></i>{{ link.name }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div id="club-page">
|
|
||||||
{% if page_revision %}
|
|
||||||
{{ page_revision|markdown }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -62,18 +62,6 @@
|
|||||||
{{ club.name }} {% if not club.is_active %}({% trans %}inactive{% endtrans %}){% endif %}
|
{{ club.name }} {% if not club.is_active %}({% trans %}inactive{% endtrans %}){% endif %}
|
||||||
</h4>
|
</h4>
|
||||||
</a>
|
</a>
|
||||||
{% set links = club.links.all() %}
|
|
||||||
{% if links %}
|
|
||||||
<br>
|
|
||||||
<div class="row gap-2x">
|
|
||||||
{% for link in club.links.all() %}
|
|
||||||
<a href="{{ link.url }}">
|
|
||||||
<i class="{{ link.link_type.icon }} fa-xl"></i>
|
|
||||||
<strong>{{ link.name }}</strong>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{{ club.short_description|markdown }}
|
{{ club.short_description|markdown }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,15 @@
|
|||||||
|
|
||||||
<h2>{% trans %}Club members{% endtrans %}</h2>
|
<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"
|
||||||
|
>
|
||||||
|
<i class="fa fa-users-gear"></i> {% trans %}Manage roles{% endtrans %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if add_member_fragment %}
|
{% if add_member_fragment %}
|
||||||
<br />
|
<br />
|
||||||
{{ add_member_fragment }}
|
{{ add_member_fragment }}
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
|
{% block additional_js %}
|
||||||
|
<script type="module" src="{{ static("bundled/club/role-list-index.ts") }}" xmlns="http://www.w3.org/1999/html"></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-grey margin-bottom" href="{{ url("club:new_role_president", club_id=club.id) }}">
|
||||||
|
<i class="fa fa-plus"></i> {% trans %}add role{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<details class="clickable margin-bottom">
|
||||||
|
<summary>{% trans %}Help{% endtrans %}</summary>
|
||||||
|
{# 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>
|
||||||
|
</details>
|
||||||
|
<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>
|
||||||
|
<br>
|
||||||
|
<h3>{% trans %}Board{% endtrans %}</h3>
|
||||||
|
<a class="btn btn-grey margin-bottom" href="{{ url("club:new_role_board", club_id=club.id) }}">
|
||||||
|
<i class="fa fa-plus"></i> {% trans %}add role{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<details class="clickable margin-bottom">
|
||||||
|
<summary>{% trans %}Help{% endtrans %}</summary>
|
||||||
|
<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>
|
||||||
|
</details>
|
||||||
|
<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>
|
||||||
|
<br>
|
||||||
|
<h3>{% trans %}Members{% endtrans %}</h3>
|
||||||
|
<a class="btn btn-grey margin-bottom" href="{{ url("club:new_role_member", club_id=club.id) }}">
|
||||||
|
<i class="fa fa-plus"></i> {% trans %}add role{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<details class="clickable margin-bottom">
|
||||||
|
<summary>{% trans %}Help{% endtrans %}</summary>
|
||||||
|
<div class="markdown">
|
||||||
|
<p>{% trans %}Simple members cannot perform administrative actions.{% endtrans %}</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<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>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
<button type="submit" class="btn btn-blue">
|
||||||
|
<i class="fa fa-check"></i>{% trans %}Save{% endtrans %}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
@@ -5,8 +5,19 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4>{% trans %}Communication:{% endtrans %}</h4>
|
<h4>{% trans %}Communication:{% endtrans %}</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li> <a href="{{ url('com:news_new') }}?club={{ object.id }}">{% trans %}Create a news{% endtrans %}</a></li>
|
<li>
|
||||||
<li> <a href="{{ url('com:weekmail_article') }}?club={{ object.id }}">{% trans %}Post in the Weekmail{% endtrans %}</a></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 %}
|
{% if object.trombi %}
|
||||||
<li> <a href="{{ url('trombi:detail', trombi_id=object.trombi.id) }}">{% trans %}Edit Trombi{% endtrans %}</a></li>
|
<li> <a href="{{ url('trombi:detail', trombi_id=object.trombi.id) }}">{% trans %}Edit Trombi{% endtrans %}</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -1,60 +1,9 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block additional_js %}
|
|
||||||
<script type="module" src="{{ static("bundled/core/dynamic-formset-index.ts") }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans name=object %}Edit {{ name }}{% endtrans %}
|
{% trans name=object %}Edit {{ name }}{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% macro link_form(form) %}
|
|
||||||
<fieldset
|
|
||||||
{# set url in x-init rather than in x-data,
|
|
||||||
in order to trigger the $watch on initial load #}
|
|
||||||
x-data="{ url: '', linkType: { icon: '', id: 0 } }"
|
|
||||||
x-init="() => {
|
|
||||||
$watch('url', (u) => linkType = linkTypes.find((t) => u.startsWith(t.url)));
|
|
||||||
url = '{{ form.url.value() or "" }}';
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ form.non_field_errors() }}
|
|
||||||
<div class="form-group row gap-2x">
|
|
||||||
<div>
|
|
||||||
{{ form.url.label_tag() }}
|
|
||||||
{{ form.url.errors }}
|
|
||||||
<span>
|
|
||||||
{# we change the icon when the user change it and leave the input,
|
|
||||||
or when it is pasted from the clipboard #}
|
|
||||||
{{ form.url|add_attr("x-model.change=url,@paste.prevent=url = $event.clipboardData.getData('text')") }}
|
|
||||||
<i :class="linkType.icon || 'fa fa-link'"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>{{ form.name.as_field_group() }}</div>
|
|
||||||
</div>
|
|
||||||
{%- if form.DELETE -%}
|
|
||||||
<div class="form-group row gap">
|
|
||||||
{{ form.DELETE.as_field_group() }}
|
|
||||||
</div>
|
|
||||||
{%- else -%}
|
|
||||||
<br>
|
|
||||||
<button
|
|
||||||
class="btn btn-grey"
|
|
||||||
@click.prevent="removeForm($event.target.closest('fieldset'))"
|
|
||||||
>
|
|
||||||
<i class="fa fa-minus"></i> {% trans %}Remove link{% endtrans %}
|
|
||||||
</button>
|
|
||||||
{%- endif -%}
|
|
||||||
{{ form.link_type|add_attr(":value=linkType.id") }}
|
|
||||||
{%- for field in form.hidden_fields() -%}
|
|
||||||
{%- if field != form.link_type -%}
|
|
||||||
{{ field }}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
</fieldset>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{% trans name=object %}Edit {{ name }}{% endtrans %}</h2>
|
<h2>{% trans name=object %}Edit {{ name }}{% endtrans %}</h2>
|
||||||
|
|
||||||
@@ -68,7 +17,7 @@
|
|||||||
and explicitly separate them from the non-admin ones,
|
and explicitly separate them from the non-admin ones,
|
||||||
with some help text.
|
with some help text.
|
||||||
Non-admin users will only see the regular form fields,
|
Non-admin users will only see the regular form fields,
|
||||||
so they don't need those explanations #}
|
so they don't need thoses explanations #}
|
||||||
<h3>{% trans %}Club properties{% endtrans %}</h3>
|
<h3>{% trans %}Club properties{% endtrans %}</h3>
|
||||||
<p class="helptext">
|
<p class="helptext">
|
||||||
{% trans trimmed %}
|
{% trans trimmed %}
|
||||||
@@ -76,7 +25,7 @@
|
|||||||
Only admin users can see and edit them.
|
Only admin users can see and edit them.
|
||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
<fieldset class="margin-bottom">
|
<fieldset class="required margin-bottom">
|
||||||
{% for field_name in form.admin_fields %}
|
{% for field_name in form.admin_fields %}
|
||||||
{% set field = form[field_name] %}
|
{% set field = form[field_name] %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -87,13 +36,11 @@
|
|||||||
{# Remove the the admin fields from the form.
|
{# Remove the the admin fields from the form.
|
||||||
The remaining non-admin fields will be rendered
|
The remaining non-admin fields will be rendered
|
||||||
at once with a simple {{ form.as_p() }} #}
|
at once with a simple {{ form.as_p() }} #}
|
||||||
{% do form.fields.pop(field_name) %}
|
{% set _ = form.fields.pop(field_name) %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h3>{% trans %}Club informations{% endtrans %}</h3>
|
<h3>{% trans %}Club informations{% endtrans %}</h3>
|
||||||
{% if form.admin_fields %}
|
|
||||||
<p class="helptext">
|
<p class="helptext">
|
||||||
{% trans trimmed %}
|
{% trans trimmed %}
|
||||||
The following form fields are linked to the basic description of a club.
|
The following form fields are linked to the basic description of a club.
|
||||||
@@ -101,45 +48,7 @@
|
|||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<fieldset class="margin-bottom">
|
{{ form.as_p() }}
|
||||||
{{ form.as_p() }}
|
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<h3>{% trans %}Club links{% endtrans %}</h3>
|
|
||||||
<div x-data="dynamicFormSet({ prefix: '{{ form.link_formset.prefix }}' })" class="margin-bottom">
|
|
||||||
{{ form.link_formset.management_form }}
|
|
||||||
<div x-ref="formContainer">
|
|
||||||
{%- for f in form.link_formset.forms -%}
|
|
||||||
{{ link_form(f) }}
|
|
||||||
{%- endfor -%}
|
|
||||||
</div>
|
|
||||||
<template x-ref="formTemplate">
|
|
||||||
{{ link_form(form.link_formset.empty_form) }}
|
|
||||||
</template>
|
|
||||||
<p>
|
|
||||||
<i>{% trans trimmed %}
|
|
||||||
Note: if the icon of one of your links doesn't exist yet,
|
|
||||||
you can ask the info team to add it.
|
|
||||||
{% endtrans %}</i>
|
|
||||||
</p>
|
|
||||||
<br>
|
|
||||||
<button @click.prevent="addForm()" class="btn btn-grey">
|
|
||||||
<i class="fa fa-plus"></i>{% trans %}Add link{% endtrans %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<button type="submit" class="btn btn-blue">
|
|
||||||
<i class="fa fa-check"></i>{% trans %}Save{% endtrans %}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block script %}
|
|
||||||
<script>
|
|
||||||
const linkTypes = [
|
|
||||||
{%- for t in link_types -%}
|
|
||||||
{ id: {{ t.id }}, url: '{{ t.url_base }}', icon: '{{ t.icon }}' },
|
|
||||||
{%- endfor -%}
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ class TestFetchClub:
|
|||||||
def test_fetch_club_nb_queries(self, client: Client, club: Club):
|
def test_fetch_club_nb_queries(self, client: Client, club: Club):
|
||||||
user = subscriber_user.make()
|
user = subscriber_user.make()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
with assertNumQueries(7):
|
with assertNumQueries(6):
|
||||||
# - 4 queries for authentication
|
# - 4 queries for authentication
|
||||||
# - 3 queries for the actual data
|
# - 2 queries for the actual data
|
||||||
res = client.get(reverse("api:fetch_club", kwargs={"club_id": club.id}))
|
res = client.get(reverse("api:fetch_club", kwargs={"club_id": club.id}))
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
|
|||||||
+228
-8
@@ -1,25 +1,48 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
import pytest
|
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 import baker, seq
|
||||||
from model_bakery.recipe import Recipe
|
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 AnonymousUser, User
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
def make_club():
|
||||||
def test_order_auto():
|
# unittest-style tests cannot use fixture, so we create a function
|
||||||
"""Test that newly created roles are put in the right place."""
|
# that will be callable either by a pytest fixture or inside
|
||||||
|
# a TestCase.setUpTestData method.
|
||||||
club = baker.make(Club)
|
club = baker.make(Club)
|
||||||
recipe = Recipe(ClubRole, club=club, name=seq("role "))
|
recipe = Recipe(ClubRole, club=club, name=seq("role "))
|
||||||
# bulk create initial roles (1 presidency, 1 board, 1 member)
|
recipe.make(
|
||||||
roles = recipe.make(
|
|
||||||
is_board=iter([True, True, False]),
|
is_board=iter([True, True, False]),
|
||||||
is_presidency=iter([True, False, False]),
|
is_presidency=iter([True, False, False]),
|
||||||
order=iter([1, 2, 3]),
|
order=iter([0, 1, 2]),
|
||||||
_quantity=3,
|
_quantity=3,
|
||||||
_bulk_create=True,
|
_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
|
# 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_a = recipe.make(is_board=True, is_presidency=True, order=None)
|
||||||
role_b = recipe.make(is_board=True, is_presidency=False, 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)
|
role_c = recipe.make(is_board=False, is_presidency=False, order=None)
|
||||||
@@ -31,3 +54,200 @@ def test_order_auto():
|
|||||||
roles[2],
|
roles[2],
|
||||||
role_c,
|
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,
|
||||||
|
),
|
||||||
|
(lambda _: AnonymousUser(), 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})
|
||||||
|
cls.redirect_url = reverse("club:club_members", 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": 1,
|
||||||
|
"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": 2,
|
||||||
|
"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": 3,
|
||||||
|
"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.redirect_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",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_president_moves_itself_out_of_the_presidency(self):
|
||||||
|
"""Test that if the user moves its own role out of the presidency,
|
||||||
|
then it's redirected to another page and loses access to the update page."""
|
||||||
|
self.payload["roles-0-is_presidency"] = False
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
res = self.client.post(self.url, data=self.payload)
|
||||||
|
assertRedirects(res, self.redirect_url)
|
||||||
|
# When the user clicked that button, it still had the right to update roles,
|
||||||
|
# so the modification should be applied
|
||||||
|
self.roles[0].refresh_from_db()
|
||||||
|
assert self.roles[0].is_presidency is False
|
||||||
|
|
||||||
|
res = self.client.get(self.url)
|
||||||
|
assert res.status_code == 403
|
||||||
|
|||||||
@@ -21,13 +21,7 @@ def test_club_board_member_cannot_edit_club_properties(client: Client):
|
|||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
res = client.post(
|
res = client.post(
|
||||||
reverse("club:club_edit", kwargs={"club_id": club.id}),
|
reverse("club:club_edit", kwargs={"club_id": club.id}),
|
||||||
{
|
{"name": "new name", "is_active": False, "address": "new address"},
|
||||||
"name": "new name",
|
|
||||||
"is_active": False,
|
|
||||||
"address": "new address",
|
|
||||||
"link-TOTAL_FORMS": 0,
|
|
||||||
"link-INITIAL_FORMS": 0,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
# The request should success,
|
# The request should success,
|
||||||
# but admin-only fields shouldn't be taken into account
|
# but admin-only fields shouldn't be taken into account
|
||||||
|
|||||||
@@ -317,6 +317,51 @@ class TestMembership(TestClub):
|
|||||||
self.club.refresh_from_db()
|
self.club.refresh_from_db()
|
||||||
assert self.club.members.count() == nb_memberships
|
assert self.club.members.count() == nb_memberships
|
||||||
|
|
||||||
|
def test_president_add_members(self):
|
||||||
|
"""Test that the president of the club can add members."""
|
||||||
|
president = self.club.members.get(role=self.president_role).user
|
||||||
|
nb_club_membership = self.club.members.count()
|
||||||
|
nb_subscriber_memberships = self.subscriber.memberships.count()
|
||||||
|
self.client.force_login(president)
|
||||||
|
response = self.client.post(
|
||||||
|
self.new_members_url,
|
||||||
|
{"user": self.subscriber.id, "role": self.president_role.id},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers.get("HX-Redirect", "") == reverse(
|
||||||
|
"club:club_members", kwargs={"club_id": self.club.id}
|
||||||
|
)
|
||||||
|
self.club.refresh_from_db()
|
||||||
|
self.subscriber.refresh_from_db()
|
||||||
|
assert self.club.members.count() == nb_club_membership + 1
|
||||||
|
assert self.subscriber.memberships.count() == nb_subscriber_memberships + 1
|
||||||
|
self.assert_membership_started_today(self.subscriber, role=self.president_role)
|
||||||
|
|
||||||
|
def test_add_member_greater_role(self):
|
||||||
|
"""Test that a member of the club member cannot create
|
||||||
|
a membership with a greater role than its own.
|
||||||
|
"""
|
||||||
|
user_role = self.simple_board_member.memberships.first().role
|
||||||
|
other_role = baker.make(ClubRole, club=user_role.club, is_board=True)
|
||||||
|
other_role.above(user_role)
|
||||||
|
form = ClubAddMemberForm(
|
||||||
|
data={"user": self.subscriber.id, "role": other_role.id},
|
||||||
|
request_user=self.simple_board_member,
|
||||||
|
club=self.club,
|
||||||
|
)
|
||||||
|
nb_memberships = self.club.members.count()
|
||||||
|
|
||||||
|
assert not form.is_valid()
|
||||||
|
assert form.errors == {
|
||||||
|
"role": [
|
||||||
|
"Sélectionnez un choix valide. "
|
||||||
|
"Ce choix ne fait pas partie de ceux disponibles."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
self.club.refresh_from_db()
|
||||||
|
assert nb_memberships == self.club.members.count()
|
||||||
|
assert not self.subscriber.memberships.filter(club=self.club).exists()
|
||||||
|
|
||||||
def test_add_member_without_role(self):
|
def test_add_member_without_role(self):
|
||||||
"""Test that trying to add members without specifying their role fails."""
|
"""Test that trying to add members without specifying their role fails."""
|
||||||
form = ClubAddMemberForm(
|
form = ClubAddMemberForm(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def test_page_display_on_club_main_page(client: Client):
|
|||||||
|
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
soup = BeautifulSoup(res.text, "lxml")
|
soup = BeautifulSoup(res.text, "lxml")
|
||||||
detail_html = soup.find(id="club-page").find(class_="markdown")
|
detail_html = soup.find(id="club_detail").find(class_="markdown")
|
||||||
assertHTMLEqual(detail_html.decode_contents(), markdown(content))
|
assertHTMLEqual(detail_html.decode_contents(), markdown(content))
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ def test_club_main_page_without_content(client: Client):
|
|||||||
|
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
soup = BeautifulSoup(res.text, "lxml")
|
soup = BeautifulSoup(res.text, "lxml")
|
||||||
detail_html = soup.find(id="club-page")
|
detail_html = soup.find(id="club_detail")
|
||||||
assert detail_html.find_all("markdown") == []
|
assert detail_html.find_all("markdown") == []
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ from club.views import (
|
|||||||
ClubPageEditView,
|
ClubPageEditView,
|
||||||
ClubPageHistView,
|
ClubPageHistView,
|
||||||
ClubRevView,
|
ClubRevView,
|
||||||
|
ClubRoleBoardCreateView,
|
||||||
|
ClubRoleMemberCreateView,
|
||||||
|
ClubRolePresidencyCreateView,
|
||||||
|
ClubRoleUpdateView,
|
||||||
ClubSellingCSVView,
|
ClubSellingCSVView,
|
||||||
ClubSellingView,
|
ClubSellingView,
|
||||||
ClubToolsView,
|
ClubToolsView,
|
||||||
@@ -71,6 +75,22 @@ urlpatterns = [
|
|||||||
ClubOldMembersView.as_view(),
|
ClubOldMembersView.as_view(),
|
||||||
name="club_old_members",
|
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/", ClubSellingView.as_view(), name="club_sellings"),
|
||||||
path(
|
path(
|
||||||
"<int:club_id>/sellings/csv/", ClubSellingCSVView.as_view(), name="sellings_csv"
|
"<int:club_id>/sellings/csv/", ClubSellingCSVView.as_view(), name="sellings_csv"
|
||||||
|
|||||||
+123
-20
@@ -28,12 +28,15 @@ import csv
|
|||||||
import itertools
|
import itertools
|
||||||
from typing import TYPE_CHECKING, Any
|
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.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
|
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
|
||||||
from django.core.paginator import InvalidPage, Paginator
|
from django.core.paginator import InvalidPage, Paginator
|
||||||
from django.db.models import F, Prefetch, Q, Sum
|
from django.db.models import F, Q, Sum
|
||||||
from django.db.models.functions import Length
|
|
||||||
from django.http import Http404, StreamingHttpResponse
|
from django.http import Http404, StreamingHttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
@@ -56,19 +59,14 @@ from club.forms import (
|
|||||||
ClubAdminEditForm,
|
ClubAdminEditForm,
|
||||||
ClubEditForm,
|
ClubEditForm,
|
||||||
ClubOldMemberForm,
|
ClubOldMemberForm,
|
||||||
|
ClubRoleCreateForm,
|
||||||
|
ClubRoleFormSet,
|
||||||
ClubSearchForm,
|
ClubSearchForm,
|
||||||
JoinClubForm,
|
JoinClubForm,
|
||||||
MailingForm,
|
MailingForm,
|
||||||
SellingsForm,
|
SellingsForm,
|
||||||
)
|
)
|
||||||
from club.models import (
|
from club.models import Club, ClubRole, Mailing, MailingSubscription, Membership
|
||||||
Club,
|
|
||||||
ClubLink,
|
|
||||||
LinkType,
|
|
||||||
Mailing,
|
|
||||||
MailingSubscription,
|
|
||||||
Membership,
|
|
||||||
)
|
|
||||||
from com.models import Poster
|
from com.models import Poster
|
||||||
from com.views import (
|
from com.views import (
|
||||||
PosterCreateBaseView,
|
PosterCreateBaseView,
|
||||||
@@ -212,9 +210,7 @@ class ClubListView(AllowFragment, FormMixin, ListView):
|
|||||||
|
|
||||||
template_name = "club/club_list.jinja"
|
template_name = "club/club_list.jinja"
|
||||||
form_class = ClubSearchForm
|
form_class = ClubSearchForm
|
||||||
queryset = Club.objects.prefetch_related(
|
queryset = Club.objects.order_by("name")
|
||||||
Prefetch("links", queryset=ClubLink.objects.select_related("link_type"))
|
|
||||||
).order_by("name")
|
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
@@ -253,7 +249,6 @@ class ClubView(ClubTabsMixin, DetailView):
|
|||||||
.values_list("content", flat=True)
|
.values_list("content", flat=True)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
kwargs["links"] = list(self.object.links.select_related("link_type").all())
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@@ -425,6 +420,119 @@ 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")
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def club(self) -> Club:
|
||||||
|
return self.get_object()
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
return self.club.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 reverse("club:club_members", kwargs={"club_id": self.club.id})
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super().get_context_data(**kwargs) | {
|
||||||
|
"user_role": ClubRole.objects.filter(
|
||||||
|
club=self.club,
|
||||||
|
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):
|
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||||
"""Sales of a club."""
|
"""Sales of a club."""
|
||||||
|
|
||||||
@@ -581,11 +689,6 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
|
|||||||
return ClubAdminEditForm
|
return ClubAdminEditForm
|
||||||
return ClubEditForm
|
return ClubEditForm
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
return super().get_context_data(**kwargs) | {
|
|
||||||
"link_types": list(LinkType.objects.order_by(Length("url_base").desc()))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ClubCreateView(PermissionRequiredMixin, CreateView):
|
class ClubCreateView(PermissionRequiredMixin, CreateView):
|
||||||
"""Create a club (for the Sith admin)."""
|
"""Create a club (for the Sith admin)."""
|
||||||
|
|||||||
@@ -16,13 +16,9 @@
|
|||||||
#right_column {
|
#right_column {
|
||||||
flex: 20%;
|
flex: 20%;
|
||||||
margin: 3.2px;
|
margin: 3.2px;
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|
||||||
@media screen and (min-width: 800px) {
|
|
||||||
max-width: 20%;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#left_column {
|
#left_column {
|
||||||
@@ -50,7 +46,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: $small-devices) {
|
||||||
#left_column,
|
#left_column,
|
||||||
#right_column {
|
#right_column {
|
||||||
flex: 100%;
|
flex: 100%;
|
||||||
@@ -80,10 +76,10 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: white;
|
background: white;
|
||||||
|
font-size: 70%;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
|
||||||
#links_content {
|
#links_content {
|
||||||
font-size: 85%;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
box-shadow: $shadow-color 1px 1px 1px;
|
box-shadow: $shadow-color 1px 1px 1px;
|
||||||
min-height: 20em;
|
min-height: 20em;
|
||||||
@@ -99,10 +95,24 @@
|
|||||||
|
|
||||||
li {
|
li {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
|
||||||
|
.fa-facebook {
|
||||||
|
color: $faceblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-discord {
|
||||||
|
color: $discordblurple;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-square-instagram::before {
|
||||||
|
background: $instagradient;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-right: .5rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from django.utils import timezone
|
|||||||
from django.utils.timezone import localdate
|
from django.utils.timezone import localdate
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from club.models import Club, ClubLink, ClubRole, LinkType, Membership
|
from club.models import Club, ClubRole, Membership
|
||||||
from com.ics_calendar import IcsCalendar
|
from com.ics_calendar import IcsCalendar
|
||||||
from com.models import News, NewsDate, Sith, Weekmail
|
from com.models import News, NewsDate, Sith, Weekmail
|
||||||
from core.models import BanGroup, Group, Page, PageRev, SithFile, User
|
from core.models import BanGroup, Group, Page, PageRev, SithFile, User
|
||||||
@@ -830,54 +830,6 @@ class Command(BaseCommand):
|
|||||||
):
|
):
|
||||||
roles.append(ClubRole(club=club, order=i, name=role))
|
roles.append(ClubRole(club=club, order=i, name=role))
|
||||||
ClubRole.objects.bulk_create(roles)
|
ClubRole.objects.bulk_create(roles)
|
||||||
insta, fb, discord, _ = LinkType.objects.bulk_create(
|
|
||||||
[
|
|
||||||
LinkType(
|
|
||||||
name="instagram",
|
|
||||||
icon="fa-brands fa-square-instagram",
|
|
||||||
url_base="https://www.instagram.com",
|
|
||||||
),
|
|
||||||
LinkType(
|
|
||||||
name="facebook",
|
|
||||||
icon="fa-brands fa-facebook",
|
|
||||||
url_base="https://www.facebook.com",
|
|
||||||
),
|
|
||||||
LinkType(
|
|
||||||
name="discord",
|
|
||||||
icon="fa-brands fa-discord",
|
|
||||||
url_base="https://discord.gg",
|
|
||||||
),
|
|
||||||
LinkType(name="generic", icon="fa fa-link", url_base=""),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
ClubLink.objects.bulk_create(
|
|
||||||
[
|
|
||||||
ClubLink(
|
|
||||||
name="insta AE",
|
|
||||||
url="https://www.instagram.com/ae_utbm/",
|
|
||||||
club=ae,
|
|
||||||
link_type=insta,
|
|
||||||
),
|
|
||||||
ClubLink(
|
|
||||||
name="insta activités AE",
|
|
||||||
url="https://www.instagram.com/activites_ae/",
|
|
||||||
club=ae,
|
|
||||||
link_type=insta,
|
|
||||||
),
|
|
||||||
ClubLink(
|
|
||||||
name="facebook AE",
|
|
||||||
url="https://www.facebook.com/ae_utbm",
|
|
||||||
club=ae,
|
|
||||||
link_type=fb,
|
|
||||||
),
|
|
||||||
ClubLink(
|
|
||||||
name="Discord",
|
|
||||||
url="https://discord.gg/QvTm3XJrHR",
|
|
||||||
club=ae,
|
|
||||||
link_type=discord,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return PopulatedClubs(ae=ae, troll=troll, pdf=pdf, refound=refound)
|
return PopulatedClubs(ae=ae, troll=troll, pdf=pdf, refound=refound)
|
||||||
|
|
||||||
def _create_groups(self) -> PopulatedGroups:
|
def _create_groups(self) -> PopulatedGroups:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ details.accordion>.accordion-content {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
@supports (max-height: calc-size(max-content, size)) {
|
@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
|
// we use .accordion-content as a workaround
|
||||||
// But we need to use ::details-content for chrome because it's
|
// But we need to use ::details-content for chrome because it's
|
||||||
// not working correctly otherwise
|
// 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
|
// Note: `selector` is not supported by scss so we comment it out to
|
||||||
// avoid compiling it and sending it straight to the css
|
// avoid compiling it and sending it straight to the css
|
||||||
// This is a trick that comes from here :
|
// This is a trick that comes from here :
|
||||||
|
|||||||
+13
-22
@@ -398,28 +398,6 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fontawesome icons */
|
|
||||||
.fa-brands, .fa-link {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-facebook {
|
|
||||||
color: $faceblue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-discord {
|
|
||||||
color: $discordblurple;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-square-instagram::before, .fa-instagram::before {
|
|
||||||
background: $instagradient;
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-bluesky, .fa-square-bluesky {
|
|
||||||
color: #0f73ff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $small-devices) {
|
@media screen and (max-width: $small-devices) {
|
||||||
@@ -771,3 +749,16 @@ textarea {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*--------------------------------JQuery-------------------------------*/
|
||||||
|
#club_detail {
|
||||||
|
.club_logo {
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
max-height: 10em;
|
||||||
|
max-width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% 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 %}
|
{% block title %}
|
||||||
{% trans name=form.instance.__class__._meta.verbose_name %}Create {{ name }}{% endtrans %}
|
{% trans name=object_name %}Create {{ name }}{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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">
|
<form action="" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_p() }}
|
{{ form.as_p() }}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ def add_attr(field: BoundField, attr: str):
|
|||||||
if "=" not in d:
|
if "=" not in d:
|
||||||
attrs["class"] = d
|
attrs["class"] = d
|
||||||
else:
|
else:
|
||||||
key, val = d.split("=", maxsplit=1)
|
key, val = d.split("=")
|
||||||
attrs[key] = val
|
attrs[key] = val
|
||||||
|
|
||||||
return field.as_widget(attrs=attrs)
|
return field.as_widget(attrs=attrs)
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"result",
|
"result",
|
||||||
models.OneToOneField(
|
models.OneToOneField(
|
||||||
help_text="The product got with the formula.",
|
help_text="The formula product.",
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="formula",
|
|
||||||
to="counter.product",
|
to="counter.product",
|
||||||
verbose_name="result product",
|
verbose_name="result product",
|
||||||
),
|
),
|
||||||
|
|||||||
+162
-110
@@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-05-12 11:12+0200\n"
|
"POT-Creation-Date: 2026-05-12 09:52+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"
|
||||||
@@ -270,12 +270,9 @@ msgid "club roles"
|
|||||||
msgstr "rôles de club"
|
msgstr "rôles de club"
|
||||||
|
|
||||||
#: club/models.py
|
#: club/models.py
|
||||||
#, python-format
|
msgid "A role cannot be in the presidency while not being in the board"
|
||||||
msgid ""
|
|
||||||
"Role %(name)s was declared as a presidency role without being a board role"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Le rôle %(name)s a été déclaré comme rôle de présidence sans être un rôle du "
|
"Un rôle ne peut pas appartenir à la présidence sans être dans le bureau"
|
||||||
"bureau."
|
|
||||||
|
|
||||||
#: club/models.py
|
#: club/models.py
|
||||||
#, python-format
|
#, python-format
|
||||||
@@ -287,7 +284,8 @@ msgstr ""
|
|||||||
#, python-format
|
#, python-format
|
||||||
msgid "Role %(role)s cannot be placed below a non-presidency role"
|
msgid "Role %(role)s cannot be placed below a non-presidency role"
|
||||||
msgstr ""
|
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
|
#: club/models.py core/models.py counter/models.py eboutic/models.py
|
||||||
#: election/models.py pedagogy/models.py sas/models.py trombi/models.py
|
#: election/models.py pedagogy/models.py sas/models.py trombi/models.py
|
||||||
@@ -364,58 +362,6 @@ msgstr "Cet email est déjà abonné à cette mailing"
|
|||||||
msgid "Unregistered user"
|
msgid "Unregistered user"
|
||||||
msgstr "Utilisateur non enregistré"
|
msgstr "Utilisateur non enregistré"
|
||||||
|
|
||||||
#: club/models.py
|
|
||||||
#, python-format
|
|
||||||
msgid "The base url that links with this type must respect (e.g. `%(url)s`)"
|
|
||||||
msgstr ""
|
|
||||||
"L'url de base que tous les liens de ce type doivent respecter (par exemple "
|
|
||||||
"`%(url)s`)"
|
|
||||||
|
|
||||||
#: club/models.py counter/models.py
|
|
||||||
msgid "icon"
|
|
||||||
msgstr "icône"
|
|
||||||
|
|
||||||
#: club/models.py
|
|
||||||
msgid "The fontawesome class to use (e.g. `fa-brands fa-instagram`)"
|
|
||||||
msgstr ""
|
|
||||||
"La classe fontawesome à utiliser (par exemple `fa-brands fa-instagram`)"
|
|
||||||
|
|
||||||
#: club/models.py
|
|
||||||
msgid "link type"
|
|
||||||
msgstr "type de lien"
|
|
||||||
|
|
||||||
#: club/models.py
|
|
||||||
msgid "link types"
|
|
||||||
msgstr "types de lien"
|
|
||||||
|
|
||||||
#: club/models.py
|
|
||||||
msgid "link url"
|
|
||||||
msgstr "url du lien"
|
|
||||||
|
|
||||||
#: club/models.py core/models.py counter/models.py
|
|
||||||
msgid "created at"
|
|
||||||
msgstr "créé le"
|
|
||||||
|
|
||||||
#: club/models.py core/models.py counter/models.py
|
|
||||||
msgid "updated at"
|
|
||||||
msgstr "mis à jour le"
|
|
||||||
|
|
||||||
#: club/models.py
|
|
||||||
msgid "club link"
|
|
||||||
msgstr "lien de club"
|
|
||||||
|
|
||||||
#: club/models.py
|
|
||||||
msgid "club links"
|
|
||||||
msgstr "liens de club"
|
|
||||||
|
|
||||||
#: club/models.py
|
|
||||||
msgid "This link doesn't match with the url base of its type."
|
|
||||||
msgstr "Ce lien ne correspond pas à l'url de base de son type."
|
|
||||||
|
|
||||||
#: club/templates/club/club_detail.jinja com/templates/com/news_list.jinja
|
|
||||||
msgid "Links"
|
|
||||||
msgstr "Liens"
|
|
||||||
|
|
||||||
#: club/templates/club/club_list.jinja
|
#: club/templates/club/club_list.jinja
|
||||||
msgid "The list of all clubs existing at UTBM."
|
msgid "The list of all clubs existing at UTBM."
|
||||||
msgstr "La liste de tous les clubs existants à l'UTBM"
|
msgstr "La liste de tous les clubs existants à l'UTBM"
|
||||||
@@ -437,7 +383,7 @@ msgstr "Recherche"
|
|||||||
msgid "New club"
|
msgid "New club"
|
||||||
msgstr "Nouveau club"
|
msgstr "Nouveau club"
|
||||||
|
|
||||||
#: club/templates/club/club_list.jinja
|
#: club/templates/club/club_list.jinja club/templates/club/club_roles.jinja
|
||||||
msgid "inactive"
|
msgid "inactive"
|
||||||
msgstr "inactif"
|
msgstr "inactif"
|
||||||
|
|
||||||
@@ -445,6 +391,10 @@ msgstr "inactif"
|
|||||||
msgid "Club members"
|
msgid "Club members"
|
||||||
msgstr "Membres du club"
|
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_members.jinja
|
||||||
#: club/templates/club/club_old_members.jinja
|
#: club/templates/club/club_old_members.jinja
|
||||||
#: core/templates/core/user_clubs.jinja
|
#: core/templates/core/user_clubs.jinja
|
||||||
@@ -483,6 +433,121 @@ msgstr "Du"
|
|||||||
msgid "To"
|
msgid "To"
|
||||||
msgstr "Au"
|
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 core/templates/core/base/navbar.jinja
|
||||||
|
msgid "Help"
|
||||||
|
msgstr "Aide"
|
||||||
|
|
||||||
|
#: 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_existing_user.jinja
|
||||||
|
#: subscription/templates/subscription/fragments/creation_form_new_user.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
|
#: club/templates/club/club_sellings.jinja
|
||||||
msgid "Previous"
|
msgid "Previous"
|
||||||
msgstr "Précédent"
|
msgstr "Précédent"
|
||||||
@@ -643,10 +708,6 @@ msgstr "Comptoirs : "
|
|||||||
msgid "Edit %(name)s"
|
msgid "Edit %(name)s"
|
||||||
msgstr "Éditer %(name)s"
|
msgstr "Éditer %(name)s"
|
||||||
|
|
||||||
#: club/templates/club/edit_club.jinja
|
|
||||||
msgid "Remove link"
|
|
||||||
msgstr "Retirer le lien"
|
|
||||||
|
|
||||||
#: club/templates/club/edit_club.jinja
|
#: club/templates/club/edit_club.jinja
|
||||||
msgid "Club properties"
|
msgid "Club properties"
|
||||||
msgstr "Propriétés du club"
|
msgstr "Propriétés du club"
|
||||||
@@ -671,44 +732,6 @@ msgstr ""
|
|||||||
"Les champs de formulaire suivants sont liées à la description basique d'un "
|
"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. Tous les membres du bureau du club peuvent voir et modifier ceux-ci."
|
||||||
|
|
||||||
#: club/templates/club/edit_club.jinja
|
|
||||||
msgid "Club links"
|
|
||||||
msgstr "Liens du club"
|
|
||||||
|
|
||||||
#: club/templates/club/edit_club.jinja
|
|
||||||
msgid ""
|
|
||||||
"Note: if the icon of one of your links doesn't exist yet, you can ask the "
|
|
||||||
"info team to add it."
|
|
||||||
msgstr ""
|
|
||||||
"Note : si l'icône d'un de vos liens n'existe pas encore, vous pouvez demander "
|
|
||||||
"au pôle info de l'ajouter."
|
|
||||||
|
|
||||||
#: club/templates/club/edit_club.jinja
|
|
||||||
msgid "Add link"
|
|
||||||
msgstr "Ajouter un lien"
|
|
||||||
|
|
||||||
#: 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_existing_user.jinja
|
|
||||||
#: subscription/templates/subscription/fragments/creation_form_new_user.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
|
#: club/templates/club/fragments/add_member.jinja
|
||||||
msgid "Add a new member"
|
msgid "Add a new member"
|
||||||
msgstr "Ajouter un nouveau membre"
|
msgstr "Ajouter un nouveau membre"
|
||||||
@@ -789,10 +812,6 @@ msgstr "Éditer la page"
|
|||||||
msgid "Infos"
|
msgid "Infos"
|
||||||
msgstr "Infos"
|
msgstr "Infos"
|
||||||
|
|
||||||
#: club/views.py
|
|
||||||
msgid "Members"
|
|
||||||
msgstr "Membres"
|
|
||||||
|
|
||||||
#: club/views.py
|
#: club/views.py
|
||||||
msgid "Old members"
|
msgid "Old members"
|
||||||
msgstr "Anciens membres"
|
msgstr "Anciens membres"
|
||||||
@@ -842,6 +861,27 @@ msgstr "Vous êtes maintenant membre de ce club."
|
|||||||
msgid "%(user)s has been added to club."
|
msgid "%(user)s has been added to club."
|
||||||
msgstr "%(user)s a été ajouté au 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
|
#: club/views.py
|
||||||
msgid "Benefit"
|
msgid "Benefit"
|
||||||
msgstr "Bénéfice"
|
msgstr "Bénéfice"
|
||||||
@@ -1211,6 +1251,10 @@ msgstr ""
|
|||||||
msgid "All coming events"
|
msgid "All coming events"
|
||||||
msgstr "Tous les événements à venir"
|
msgstr "Tous les événements à venir"
|
||||||
|
|
||||||
|
#: com/templates/com/news_list.jinja
|
||||||
|
msgid "Links"
|
||||||
|
msgstr "Liens"
|
||||||
|
|
||||||
#: com/templates/com/news_list.jinja
|
#: com/templates/com/news_list.jinja
|
||||||
msgid "Our services"
|
msgid "Our services"
|
||||||
msgstr "Nos services"
|
msgstr "Nos services"
|
||||||
@@ -1716,6 +1760,10 @@ msgstr "Visiteur"
|
|||||||
msgid "ban type"
|
msgid "ban type"
|
||||||
msgstr "type de ban"
|
msgstr "type de ban"
|
||||||
|
|
||||||
|
#: core/models.py counter/models.py
|
||||||
|
msgid "created at"
|
||||||
|
msgstr "créé le"
|
||||||
|
|
||||||
#: core/models.py
|
#: core/models.py
|
||||||
msgid "expires at"
|
msgid "expires at"
|
||||||
msgstr "expire le"
|
msgstr "expire le"
|
||||||
@@ -1805,6 +1853,10 @@ msgstr "taille"
|
|||||||
msgid "date"
|
msgid "date"
|
||||||
msgstr "date"
|
msgstr "date"
|
||||||
|
|
||||||
|
#: core/models.py counter/models.py
|
||||||
|
msgid "updated at"
|
||||||
|
msgstr "mis à jour le"
|
||||||
|
|
||||||
#: core/models.py
|
#: core/models.py
|
||||||
msgid "asked for removal"
|
msgid "asked for removal"
|
||||||
msgstr "retrait demandé"
|
msgstr "retrait demandé"
|
||||||
@@ -2057,10 +2109,6 @@ msgstr "Partenaires"
|
|||||||
msgid "Subscriber benefits"
|
msgid "Subscriber benefits"
|
||||||
msgstr "Les avantages cotisants"
|
msgstr "Les avantages cotisants"
|
||||||
|
|
||||||
#: core/templates/core/base/navbar.jinja
|
|
||||||
msgid "Help"
|
|
||||||
msgstr "Aide"
|
|
||||||
|
|
||||||
#: core/templates/core/base/navbar.jinja
|
#: core/templates/core/base/navbar.jinja
|
||||||
msgid "FAQ"
|
msgid "FAQ"
|
||||||
msgstr "FAQ"
|
msgstr "FAQ"
|
||||||
@@ -3279,6 +3327,10 @@ msgstr "prix d'achat"
|
|||||||
msgid "Initial cost of purchasing the product"
|
msgid "Initial cost of purchasing the product"
|
||||||
msgstr "Coût initial d'achat du produit"
|
msgstr "Coût initial d'achat du produit"
|
||||||
|
|
||||||
|
#: counter/models.py
|
||||||
|
msgid "icon"
|
||||||
|
msgstr "icône"
|
||||||
|
|
||||||
#: counter/models.py
|
#: counter/models.py
|
||||||
msgid "limit age"
|
msgid "limit age"
|
||||||
msgstr "âge limite"
|
msgstr "âge limite"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"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"
|
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
|
||||||
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@@ -17,6 +17,14 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\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
|
#: com/static/bundled/com/components/ics-calendar-index.ts
|
||||||
msgid "More info"
|
msgid "More info"
|
||||||
msgstr "Plus d'informations"
|
msgstr "Plus d'informations"
|
||||||
@@ -271,4 +279,5 @@ msgstr "Il n'a pas été possible de supprimer l'image"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"Wrong timetable format. Make sure you copied if from your student folder."
|
"Wrong timetable format. Make sure you copied if from your student folder."
|
||||||
msgstr ""
|
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."
|
||||||
|
|||||||
@@ -88,11 +88,6 @@ X_FRAME_OPTIONS = "SAMEORIGIN"
|
|||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
# RemovedInDjango60Warning: It's a transitional setting helpful in early
|
|
||||||
# adoption of "https" as the new default value of forms.URLField.assume_scheme.
|
|
||||||
# Remove this after upgrading to Django 6.x
|
|
||||||
FORMS_URLFIELD_ASSUME_HTTPS = True
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|||||||
Reference in New Issue
Block a user