From c9b724f8da637693dccccbcaa6ff6eaf3f966746 Mon Sep 17 00:00:00 2001 From: Quentin BEY Date: Thu, 16 Oct 2025 21:02:24 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(activation-codes)=20allow=20code=20cu?= =?UTF-8?q?stomization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The activation codes can be fully customized on creation and not edited afterward. --- src/backend/activation_codes/admin.py | 14 +++++++++++++- .../activation_codes/migrations/0001_initial.py | 6 ++++++ src/backend/activation_codes/models.py | 7 +++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/backend/activation_codes/admin.py b/src/backend/activation_codes/admin.py index 48d8cfc..106b71b 100644 --- a/src/backend/activation_codes/admin.py +++ b/src/backend/activation_codes/admin.py @@ -33,7 +33,6 @@ class ActivationCodeAdmin(admin.ModelAdmin): readonly_fields = ( "id", - "code", "current_uses", "created_at", "updated_at", @@ -79,6 +78,19 @@ class ActivationCodeAdmin(admin.ModelAdmin): actions = ["recompute_current_uses"] + def get_readonly_fields(self, request, obj=None): + """Make `code` readonly when editing an existing ActivationCode. + + When obj is None (creation form), `code` remains editable. When obj is + provided (editing), add `code` to readonly fields so it cannot be + changed after creation. + """ + # Start from the configured readonly_fields to preserve other read-only fields + ro_fields = list(self.readonly_fields) + if obj is not None: + ro_fields.append("code") + return tuple(ro_fields) + def usage_display(self, obj): """Display usage statistics.""" max_uses = obj.max_uses if obj.max_uses > 0 else "∞" diff --git a/src/backend/activation_codes/migrations/0001_initial.py b/src/backend/activation_codes/migrations/0001_initial.py index 23e918e..42fd40f 100644 --- a/src/backend/activation_codes/migrations/0001_initial.py +++ b/src/backend/activation_codes/migrations/0001_initial.py @@ -54,6 +54,12 @@ class Migration(migrations.Migration): help_text="The activation code that users will enter", max_length=50, unique=True, + validators=[ + django.core.validators.RegexValidator( + message="Code must be alphanumeric and contain no spaces or special characters", + regex="^[A-Z0-9]+$", + ) + ], verbose_name="activation code", ), ), diff --git a/src/backend/activation_codes/models.py b/src/backend/activation_codes/models.py index cc38f1d..fc8be85 100644 --- a/src/backend/activation_codes/models.py +++ b/src/backend/activation_codes/models.py @@ -7,6 +7,7 @@ import secrets import string from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator from django.db import IntegrityError, models, transaction from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -37,6 +38,12 @@ class ActivationCode(BaseModel): max_length=50, unique=True, default=generate_activation_code, + validators=[ + RegexValidator( + regex=r"^[A-Z0-9]+$", + message=_("Code must be alphanumeric and contain no spaces or special characters"), + ) + ], ) max_uses = models.PositiveIntegerField(