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 %} + + {% 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 %} +
+
+ {% for link in club.links.all() %} + + + {{ link.name }} + + {% endfor %} +
+ {% 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() }} +
+
+ {{ form.url.label_tag() }} + {{ form.url.errors }} + + {# 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')") }} + + +
+
{{ form.name.as_field_group() }}
+
+ {%- if form.DELETE -%} +
+ {{ form.DELETE.as_field_group() }} +
+ {%- else -%} +
+ + {%- 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 -%} +
+ +

+ {% 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 %} +

+
+ +
+
+ {% 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"