From 59847b3973a2a8b9e46787cf15590b5a3ad55cfe Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 27 Apr 2026 11:35:06 +0200 Subject: [PATCH] feat: ClubLink model --- club/admin.py | 17 +++- club/migrations/0017_linktype_clublink.py | 99 +++++++++++++++++++++++ club/models.py | 71 ++++++++++++++++ counter/migrations/0037_productformula.py | 3 +- sith/settings.py | 5 ++ 5 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 club/migrations/0017_linktype_clublink.py 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/migrations/0017_linktype_clublink.py b/club/migrations/0017_linktype_clublink.py new file mode 100644 index 00000000..c7904dc8 --- /dev/null +++ b/club/migrations/0017_linktype_clublink.py @@ -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"}, + ), + ] diff --git a/club/models.py b/club/models.py index f8698d6b..89818bfa 100644 --- a/club/models.py +++ b/club/models.py @@ -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.") + ) 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/sith/settings.py b/sith/settings.py index 96eba926..d57a3f90 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"