Compare commits

..

10 Commits

Author SHA1 Message Date
imperosol a4a5757835 add club links to club list page 2026-05-13 10:49:56 +02:00
imperosol 3a1f4388fd fix: incorrect initial value for ClubSearchForm.club_status 2026-05-13 10:49:56 +02:00
imperosol 3ff61e3835 add og tags to club list page 2026-05-13 10:49:56 +02:00
imperosol 2de1f9f937 add translations 2026-05-13 10:49:56 +02:00
imperosol f5eac164ec display club links on club main page 2026-05-13 10:49:56 +02:00
imperosol 2b0c36c085 feat: club link management in club edit view 2026-05-13 10:49:56 +02:00
imperosol 74a7f4ffc9 generate club links in populate 2026-05-13 10:49:56 +02:00
imperosol b9b0c00b74 feat: add links to response of GET /api/club/{club_id} 2026-05-13 10:49:56 +02:00
imperosol 59847b3973 feat: ClubLink model 2026-05-13 10:49:56 +02:00
imperosol b7275bd843 improve main page style 2026-05-12 11:07:04 +02:00
21 changed files with 647 additions and 80 deletions
+16 -1
View File
@@ -16,7 +16,7 @@ from django.contrib import admin
from django.forms.models import ModelForm
from django.http import HttpRequest
from club.models import Club, ClubRole, Membership
from club.models import Club, ClubLink, ClubRole, LinkType, Membership
@admin.register(Club)
@@ -67,3 +67,18 @@ class MembershipAdmin(admin.ModelAdmin):
"club__name",
)
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
View File
@@ -41,7 +41,7 @@ class ClubController(ControllerBase):
queryset=Membership.objects.ongoing().select_related("user", "role"),
)
return self.get_object_or_exception(
Club.objects.prefetch_related(prefetch), id=club_id
Club.objects.prefetch_related(prefetch, "links"), id=club_id
)
+42 -1
View File
@@ -28,7 +28,14 @@ from django.db.models.functions import Lower
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from club.models import Club, ClubRole, Mailing, MailingSubscription, Membership
from club.models import (
Club,
ClubLink,
ClubRole,
Mailing,
MailingSubscription,
Membership,
)
from core.models import User
from core.views.forms import SelectDateTime
from core.views.widgets.ajax_select import (
@@ -39,6 +46,26 @@ from counter.models import Counter, Selling
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):
error_css_class = "error"
required_css_class = "required"
@@ -48,6 +75,20 @@ class ClubEditForm(forms.ModelForm):
fields = ["address", "logo", "short_description"]
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):
admin_fields = ["name", "parent", "is_active"]
+99
View File
@@ -0,0 +1,99 @@
# 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"},
),
]
+71
View File
@@ -774,3 +774,74 @@ class MailingSubscription(models.Model):
def fetch_format(self):
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.")
)
+6
View File
@@ -2,6 +2,7 @@ from typing import Annotated
from django.db.models import Q
from ninja import FilterLookup, FilterSchema, ModelSchema
from pydantic import HttpUrl
from club.models import Club, ClubRole, Membership
from core.schemas import NonEmptyStr, SimpleUserSchema
@@ -62,6 +63,11 @@ class ClubSchema(ModelSchema):
fields = ["id", "name", "logo", "is_active", "short_description", "address"]
members: list[ClubMemberSchema]
links: list[HttpUrl]
@staticmethod
def resolve_links(obj: Club):
return [link.url for link in obj.links.all()]
class UserMembershipSchema(ModelSchema):
+66
View File
@@ -0,0 +1,66 @@
#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;
}
}
}
}
}
+36 -8
View File
@@ -21,15 +21,43 @@
{% endif %}
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("club/detail.scss") }}">
{% endblock %}
{% block content %}
<div id="club_detail">
{% if club.logo %}
<div class="club_logo"><img src="{{ club.logo.url }}" alt="{{ club.name }}"></div>
{% endif %}
<h3>{{ club.name }}</h3>
{% if page_revision %}
{{ page_revision|markdown }}
{% endif %}
<h3>{{ club.name }}</h3>
<div id="club-detail" {% if links %}class="has-links"{% endif %}>
<div id="club-attributes">
{% if club.logo %}
<img
class="club-logo"
src="{{ club.logo.url }}"
alt="{{ club.name }}"
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>
{% endblock %}
+19
View File
@@ -1,6 +1,13 @@
{% if is_fragment %}
{% extends "core/base_fragment.jinja" %}
{% block metatags %}
<meta property="og:url" content="{{ request.build_absolute_uri() }}" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Liste des clubs et assos" />
<meta property="og:image" content="{{ request.build_absolute_uri(static("core/img/logo_no_text.png")) }}" />
{% endblock %}
{# Don't display tabs and errors #}
{% block tabs %}
{% endblock %}
@@ -62,6 +69,18 @@
{{ club.name }} {% if not club.is_active %}({% trans %}inactive{% endtrans %}){% endif %}
</h4>
</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 }}
</div>
</div>
+97 -6
View File
@@ -1,9 +1,60 @@
{% extends "core/base.jinja" %}
{% block additional_js %}
<script type="module" src="{{ static("bundled/core/dynamic-formset-index.ts") }}"></script>
{% endblock %}
{% block title %}
{% trans name=object %}Edit {{ name }}{% endtrans %}
{% 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 %}
<h2>{% trans name=object %}Edit {{ name }}{% endtrans %}</h2>
@@ -17,7 +68,7 @@
and explicitly separate them from the non-admin ones,
with some help text.
Non-admin users will only see the regular form fields,
so they don't need thoses explanations #}
so they don't need those explanations #}
<h3>{% trans %}Club properties{% endtrans %}</h3>
<p class="helptext">
{% trans trimmed %}
@@ -25,7 +76,7 @@
Only admin users can see and edit them.
{% endtrans %}
</p>
<fieldset class="required margin-bottom">
<fieldset class="margin-bottom">
{% for field_name in form.admin_fields %}
{% set field = form[field_name] %}
<div class="form-group">
@@ -36,11 +87,13 @@
{# Remove the the admin fields from the form.
The remaining non-admin fields will be rendered
at once with a simple {{ form.as_p() }} #}
{% set _ = form.fields.pop(field_name) %}
{% do form.fields.pop(field_name) %}
{% endfor %}
</fieldset>
{% endif %}
<h3>{% trans %}Club informations{% endtrans %}</h3>
<h3>{% trans %}Club informations{% endtrans %}</h3>
{% if form.admin_fields %}
<p class="helptext">
{% trans trimmed %}
The following form fields are linked to the basic description of a club.
@@ -48,7 +101,45 @@
{% endtrans %}
</p>
{% endif %}
{{ form.as_p() }}
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
<fieldset class="margin-bottom">
{{ form.as_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>
{% 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 %}
+2 -2
View File
@@ -88,8 +88,8 @@ class TestFetchClub:
def test_fetch_club_nb_queries(self, client: Client, club: Club):
user = subscriber_user.make()
client.force_login(user)
with assertNumQueries(6):
with assertNumQueries(7):
# - 4 queries for authentication
# - 2 queries for the actual data
# - 3 queries for the actual data
res = client.get(reverse("api:fetch_club", kwargs={"club_id": club.id}))
assert res.status_code == 200
+7 -1
View File
@@ -21,7 +21,13 @@ def test_club_board_member_cannot_edit_club_properties(client: Client):
client.force_login(user)
res = client.post(
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,
# but admin-only fields shouldn't be taken into account
+2 -2
View File
@@ -21,7 +21,7 @@ def test_page_display_on_club_main_page(client: Client):
assert res.status_code == 200
soup = BeautifulSoup(res.text, "lxml")
detail_html = soup.find(id="club_detail").find(class_="markdown")
detail_html = soup.find(id="club-page").find(class_="markdown")
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
soup = BeautifulSoup(res.text, "lxml")
detail_html = soup.find(id="club_detail")
detail_html = soup.find(id="club-page")
assert detail_html.find_all("markdown") == []
+23 -7
View File
@@ -32,7 +32,8 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
from django.core.paginator import InvalidPage, Paginator
from django.db.models import F, Q, Sum
from django.db.models import F, Prefetch, Q, Sum
from django.db.models.functions import Length
from django.http import Http404, StreamingHttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
@@ -60,7 +61,14 @@ from club.forms import (
MailingForm,
SellingsForm,
)
from club.models import Club, Mailing, MailingSubscription, Membership
from club.models import (
Club,
ClubLink,
LinkType,
Mailing,
MailingSubscription,
Membership,
)
from com.models import Poster
from com.views import (
PosterCreateBaseView,
@@ -204,20 +212,22 @@ class ClubListView(AllowFragment, FormMixin, ListView):
template_name = "club/club_list.jinja"
form_class = ClubSearchForm
queryset = Club.objects.order_by("name")
queryset = Club.objects.prefetch_related(
Prefetch("links", queryset=ClubLink.objects.select_related("link_type"))
).order_by("name")
paginate_by = 20
def get_form_kwargs(self):
res = super().get_form_kwargs()
if self.request.method == "GET":
res |= {"data": self.request.GET, "initial": self.request.GET}
# if request.GET is empty, the form will interpret club_status as None,
# even though we want it to be initially True,
# so we force a defaut True value.
res["data"] = {"club_status": True} | self.request.GET.dict()
return res
def get_queryset(self):
form: ClubSearchForm = self.get_form()
qs = self.queryset
if not form.is_bound:
return qs.filter(is_active=True)
if not form.is_valid():
return qs.none()
if name := form.cleaned_data.get("name"):
@@ -243,6 +253,7 @@ class ClubView(ClubTabsMixin, DetailView):
.values_list("content", flat=True)
.first()
)
kwargs["links"] = list(self.object.links.select_related("link_type").all())
return kwargs
@@ -570,6 +581,11 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
return ClubAdminEditForm
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):
"""Create a club (for the Sith admin)."""
+8 -18
View File
@@ -16,9 +16,13 @@
#right_column {
flex: 20%;
margin: 3.2px;
display: inline-block;
vertical-align: top;
@media screen and (min-width: 800px) {
max-width: 20%;
min-width: 200px;
}
}
#left_column {
@@ -46,7 +50,7 @@
}
}
@media screen and (max-width: $small-devices) {
@media screen and (max-width: 800px) {
#left_column,
#right_column {
flex: 100%;
@@ -76,10 +80,10 @@
display: block;
width: 100%;
background: white;
font-size: 70%;
margin-bottom: 1em;
#links_content {
font-size: 85%;
overflow: auto;
box-shadow: $shadow-color 1px 1px 1px;
min-height: 20em;
@@ -95,24 +99,10 @@
li {
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 {
width: 25px;
text-align: center;
margin-right: .5rem;
}
}
}
+49 -1
View File
@@ -36,7 +36,7 @@ from django.utils import timezone
from django.utils.timezone import localdate
from PIL import Image
from club.models import Club, ClubRole, Membership
from club.models import Club, ClubLink, ClubRole, LinkType, Membership
from com.ics_calendar import IcsCalendar
from com.models import News, NewsDate, Sith, Weekmail
from core.models import BanGroup, Group, Page, PageRev, SithFile, User
@@ -830,6 +830,54 @@ class Command(BaseCommand):
):
roles.append(ClubRole(club=club, order=i, name=role))
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)
def _create_groups(self) -> PopulatedGroups:
+22 -13
View File
@@ -398,6 +398,28 @@ 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) {
@@ -749,16 +771,3 @@ textarea {
vertical-align: middle;
}
}
/*--------------------------------JQuery-------------------------------*/
#club_detail {
.club_logo {
float: right;
img {
display: block;
max-height: 10em;
max-width: 10em;
}
}
}
+1 -1
View File
@@ -103,7 +103,7 @@ def add_attr(field: BoundField, attr: str):
if "=" not in d:
attrs["class"] = d
else:
key, val = d.split("=")
key, val = d.split("=", maxsplit=1)
attrs[key] = val
return field.as_widget(attrs=attrs)
+2 -1
View File
@@ -32,8 +32,9 @@ class Migration(migrations.Migration):
(
"result",
models.OneToOneField(
help_text="The formula product.",
help_text="The product got with the formula.",
on_delete=django.db.models.deletion.CASCADE,
related_name="formula",
to="counter.product",
verbose_name="result product",
),
+73 -17
View File
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-12 09:48+0200\n"
"POT-Creation-Date: 2026-05-12 11:12+0200\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -364,6 +364,58 @@ msgstr "Cet email est déjà abonné à cette mailing"
msgid "Unregistered user"
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
msgid "The list of all clubs existing at UTBM."
msgstr "La liste de tous les clubs existants à l'UTBM"
@@ -591,6 +643,10 @@ msgstr "Comptoirs : "
msgid "Edit %(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
msgid "Club properties"
msgstr "Propriétés du club"
@@ -615,6 +671,22 @@ msgstr ""
"Les champs de formulaire suivants sont liées à la description basique d'un "
"club. Tous les membres du bureau du club peuvent voir et modifier ceux-ci."
#: club/templates/club/edit_club.jinja
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
@@ -1139,10 +1211,6 @@ msgstr ""
msgid "All coming events"
msgstr "Tous les événements à venir"
#: com/templates/com/news_list.jinja
msgid "Links"
msgstr "Liens"
#: com/templates/com/news_list.jinja
msgid "Our services"
msgstr "Nos services"
@@ -1648,10 +1716,6 @@ msgstr "Visiteur"
msgid "ban type"
msgstr "type de ban"
#: core/models.py counter/models.py
msgid "created at"
msgstr "créé le"
#: core/models.py
msgid "expires at"
msgstr "expire le"
@@ -1741,10 +1805,6 @@ msgstr "taille"
msgid "date"
msgstr "date"
#: core/models.py counter/models.py
msgid "updated at"
msgstr "mis à jour le"
#: core/models.py
msgid "asked for removal"
msgstr "retrait demandé"
@@ -3219,10 +3279,6 @@ msgstr "prix d'achat"
msgid "Initial cost of purchasing the product"
msgstr "Coût initial d'achat du produit"
#: counter/models.py
msgid "icon"
msgstr "icône"
#: counter/models.py
msgid "limit age"
msgstr "âge limite"
+5
View File
@@ -88,6 +88,11 @@ X_FRAME_OPTIONS = "SAMEORIGIN"
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
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"