diff --git a/club/admin.py b/club/admin.py
index 08883913..6d6d8bb0 100644
--- a/club/admin.py
+++ b/club/admin.py
@@ -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))
diff --git a/club/api.py b/club/api.py
index d6a8c97d..4a055e2c 100644
--- a/club/api.py
+++ b/club/api.py
@@ -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
)
diff --git a/club/forms.py b/club/forms.py
index a6268822..7c524f56 100644
--- a/club/forms.py
+++ b/club/forms.py
@@ -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"]
diff --git a/club/migrations/0017_linktype_clublink.py b/club/migrations/0017_linktype_clublink.py
new file mode 100644
index 00000000..097e77f3
--- /dev/null
+++ b/club/migrations/0017_linktype_clublink.py
@@ -0,0 +1,105 @@
+# 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, verbose_name="name")),
+ (
+ "url_base",
+ models.URLField(
+ help_text=(
+ "The base url that links with this type "
+ "must respect (e.g. `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",
+ "constraints": [
+ models.UniqueConstraint(
+ fields=["club", "url"],
+ name="club_clublink_unique_club_url",
+ violation_error_message="Duplicated url",
+ )
+ ],
+ },
+ ),
+ ]
diff --git a/club/models.py b/club/models.py
index aedc6993..6e98848e 100644
--- a/club/models.py
+++ b/club/models.py
@@ -773,3 +773,81 @@ 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)
+ 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")
+ constraints = [
+ models.UniqueConstraint(
+ fields=["club", "url"],
+ name="club_clublink_unique_club_url",
+ violation_error_message=_("Duplicated url"),
+ )
+ ]
+
+ 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.")
+ )
diff --git a/club/schemas.py b/club/schemas.py
index aa4aa4e5..99d05fc1 100644
--- a/club/schemas.py
+++ b/club/schemas.py
@@ -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):
diff --git a/club/static/club/detail.scss b/club/static/club/detail.scss
new file mode 100644
index 00000000..48ecb36f
--- /dev/null
+++ b/club/static/club/detail.scss
@@ -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;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/club/templates/club/club_detail.jinja b/club/templates/club/club_detail.jinja
index 14a5a384..bc1a1c0b 100644
--- a/club/templates/club/club_detail.jinja
+++ b/club/templates/club/club_detail.jinja
@@ -21,15 +21,43 @@
{% endif %}
{% endblock %}
+{% block additional_css %}
+
+{% endblock %}
+
{% block content %}
-
- {% if club.logo %}
-
- {% endif %}
-
{{ club.name }}
- {% if page_revision %}
- {{ page_revision|markdown }}
- {% endif %}
+
{{ club.name }}
+
+
+ {% if club.logo %}
+
+ {% endif %}
+ {% if links %}
+
+
{% trans %}Links{% endtrans %}
+
+
+ {% endif %}
+
+
+ {% if page_revision %}
+ {{ page_revision|markdown }}
+ {% endif %}
+
{% endblock %}
diff --git a/club/templates/club/club_list.jinja b/club/templates/club/club_list.jinja
index 19164143..e9e2cbd4 100644
--- a/club/templates/club/club_list.jinja
+++ b/club/templates/club/club_list.jinja
@@ -69,6 +69,18 @@
{{ club.name }} {% if not club.is_active %}({% trans %}inactive{% endtrans %}){% endif %}
+ {% set links = club.links.all() %}
+ {% if links %}
+
+
+ {% endif %}
{{ club.short_description|markdown }}
diff --git a/club/templates/club/edit_club.jinja b/club/templates/club/edit_club.jinja
index d70e2140..a8f4093e 100644
--- a/club/templates/club/edit_club.jinja
+++ b/club/templates/club/edit_club.jinja
@@ -1,9 +1,63 @@
{% extends "core/base.jinja" %}
+{% block additional_js %}
+
+{% endblock %}
+
{% block title %}
{% trans name=object %}Edit {{ name }}{% endtrans %}
{% endblock %}
+{% macro link_form(form) %}
+
+ {{ form.non_field_errors() }}
+
+ {%- if form.DELETE -%}
+
+ {{ form.DELETE.as_field_group() }}
+
+ {%- else -%}
+
+
+ {% trans %}Remove link{% endtrans %}
+
+ {%- endif -%}
+ {{ form.link_type|add_attr(":value=linkType.id") }}
+ {%- for field in form.hidden_fields() -%}
+ {%- if field != form.link_type -%}
+ {{ field }}
+ {%- endif -%}
+ {%- endfor -%}
+
+{% endmacro %}
+
+
{% block content %}
{% trans name=object %}Edit {{ name }}{% endtrans %}
@@ -17,7 +71,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 #}
{% trans %}Club properties{% endtrans %}
{% trans trimmed %}
@@ -25,7 +79,7 @@
Only admin users can see and edit them.
{% endtrans %}
-
+
{% for field_name in form.admin_fields %}
{% set field = form[field_name] %}
@@ -36,11 +90,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 %}
+ {% endif %}
-
{% trans %}Club informations{% endtrans %}
+
{% trans %}Club informations{% endtrans %}
+ {% if form.admin_fields %}
{% trans trimmed %}
The following form fields are linked to the basic description of a club.
@@ -48,7 +104,45 @@
{% endtrans %}
{% endif %}
- {{ form.as_p() }}
-
+
+ {{ form.as_p() }}
+
+
+
{% trans %}Club links{% endtrans %}
+
+ {{ form.link_formset.management_form }}
+
+ {%- for f in form.link_formset.forms -%}
+ {{ link_form(f) }}
+ {%- endfor -%}
+
+
+ {{ link_form(form.link_formset.empty_form) }}
+
+
+ {% 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 %}
+
+
+
+ {% trans %}Add link{% endtrans %}
+
+
+
+
+ {% trans %}Save{% endtrans %}
+
{% endblock content %}
+
+{% block script %}
+
+{% endblock %}
diff --git a/club/tests/test_club_controller.py b/club/tests/test_club_controller.py
index b6248e01..18d09020 100644
--- a/club/tests/test_club_controller.py
+++ b/club/tests/test_club_controller.py
@@ -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
diff --git a/club/tests/test_edit.py b/club/tests/test_edit.py
index b4d9e2b1..c8a03b61 100644
--- a/club/tests/test_edit.py
+++ b/club/tests/test_edit.py
@@ -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
diff --git a/club/tests/test_page.py b/club/tests/test_page.py
index 699a3e9e..6567a690 100644
--- a/club/tests/test_page.py
+++ b/club/tests/test_page.py
@@ -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") == []
diff --git a/club/views.py b/club/views.py
index 3e6ba5c1..6f2c7716 100644
--- a/club/views.py
+++ b/club/views.py
@@ -36,7 +36,8 @@ from django.contrib.auth.mixins import (
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
@@ -47,12 +48,7 @@ from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, View
from django.views.generic.detail import SingleObjectMixin
-from django.views.generic.edit import (
- CreateView,
- DeleteView,
- FormMixin,
- UpdateView,
-)
+from django.views.generic.edit import CreateView, DeleteView, FormMixin, UpdateView
from club.forms import (
ClubAddMemberForm,
@@ -66,7 +62,15 @@ from club.forms import (
MailingForm,
SellingsForm,
)
-from club.models import Club, ClubRole, Mailing, MailingSubscription, Membership
+from club.models import (
+ Club,
+ ClubLink,
+ ClubRole,
+ LinkType,
+ Mailing,
+ MailingSubscription,
+ Membership,
+)
from com.models import Poster
from com.views import (
PosterCreateBaseView,
@@ -210,7 +214,9 @@ 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):
@@ -249,6 +255,7 @@ class ClubView(ClubTabsMixin, DetailView):
.values_list("content", flat=True)
.first()
)
+ kwargs["links"] = list(self.object.links.select_related("link_type").all())
return kwargs
@@ -689,6 +696,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)."""
diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss
index a1dcb966..ffa928d1 100644
--- a/com/static/com/css/news-list.scss
+++ b/com/static/com/css/news-list.scss
@@ -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;
}
}
}
diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py
index d4eeaa5a..38da5e95 100644
--- a/core/management/commands/populate.py
+++ b/core/management/commands/populate.py
@@ -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:
diff --git a/core/static/core/style.scss b/core/static/core/style.scss
index 67f03898..2a1857b1 100644
--- a/core/static/core/style.scss
+++ b/core/static/core/style.scss
@@ -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;
- }
- }
-}
\ No newline at end of file
diff --git a/core/templatetags/renderer.py b/core/templatetags/renderer.py
index 927acf54..eef8bb3c 100644
--- a/core/templatetags/renderer.py
+++ b/core/templatetags/renderer.py
@@ -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)
diff --git a/counter/migrations/0037_productformula.py b/counter/migrations/0037_productformula.py
index 75fbdd7f..71ed851d 100644
--- a/counter/migrations/0037_productformula.py
+++ b/counter/migrations/0037_productformula.py
@@ -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",
),
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 2fb954ee..28f562b1 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-05-12 09:52+0200\n"
+"POT-Creation-Date: 2026-05-12 11:12+0200\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal
\n"
@@ -362,6 +362,62 @@ 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 "Duplicated url"
+msgstr "Url dupliquée"
+
+#: 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"
@@ -708,6 +764,14 @@ msgstr "Comptoirs : "
msgid "Edit %(name)s"
msgstr "Éditer %(name)s"
+#: club/templates/club/edit_club.jinja
+msgid "This icon will change according to the given url."
+msgstr "Cette icône changera en fonction de l'url fournie"
+
+#: 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"
@@ -732,6 +796,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/fragments/add_member.jinja
msgid "Add a new member"
msgstr "Ajouter un nouveau membre"
@@ -1251,10 +1331,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"
@@ -1760,10 +1836,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"
@@ -1853,10 +1925,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é"
@@ -3327,10 +3395,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"
diff --git a/sith/settings.py b/sith/settings.py
index 872c259e..9968055f 100644
--- a/sith/settings.py
+++ b/sith/settings.py
@@ -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"