Skip to content

Commit 717b8ae

Browse files
kdmccormickclaude
andcommitted
fix(squash): add code_field_check() helper for DB-level regex constraint
Adds a code_field_check(field_name, name=...) companion to code_field() that returns a CheckConstraint using Django's Regex lookup. This enforces the code regex at the database level (not just via validators), and Django also runs it as a Python-level validator automatically. Applied to Collection.collection_code as the first usage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f21785b commit 717b8ae

3 files changed

Lines changed: 55 additions & 2 deletions

File tree

src/openedx_content/applets/collections/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
from django.db import models
7171
from django.utils.translation import gettext_lazy as _
7272

73-
from openedx_django_lib.fields import MultiCollationTextField, case_insensitive_char_field, code_field
73+
from openedx_django_lib.fields import MultiCollationTextField, case_insensitive_char_field, code_field, code_field_check
7474
from openedx_django_lib.validators import validate_utc_datetime
7575

7676
from ..publishing.models import LearningPackage, PublishableEntity
@@ -179,6 +179,7 @@ class Meta:
179179
],
180180
name="oel_coll_uniq_lp_key",
181181
),
182+
code_field_check("collection_code", name="oel_coll_collection_code_regex"),
182183
]
183184
indexes = [
184185
models.Index(
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 5.2.13 on 2026-04-16 14:37
2+
3+
import django.db.models.lookups
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('openedx_content', '0009_add_collection_code_regex_validation'),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
]
14+
15+
operations = [
16+
migrations.AddConstraint(
17+
model_name='collection',
18+
constraint=models.CheckConstraint(condition=django.db.models.lookups.Regex(models.F('collection_code'), '^[a-zA-Z0-9\\-\\_\\.]+\\Z'), name='oel_coll_collection_code_regex', violation_error_message='Enter a valid "code name" consisting of letters, numbers, underscores, hyphens, or periods.'),
19+
),
20+
]

src/openedx_django_lib/fields.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from django.core.validators import RegexValidator
2020
from django.db import models
21+
from django.db.models.lookups import Regex
2122
from django.utils.translation import gettext_lazy as _
2223

2324
from .collations import MultiCollationMixin
@@ -121,9 +122,17 @@ def immutable_uuid_field() -> models.UUIDField:
121122
CODE_REGEX = re.compile(r"^[a-zA-Z0-9\-\_\.]+\Z")
122123

123124

125+
_CODE_VIOLATION_MSG = _(
126+
'Enter a valid "code name" consisting of letters, numbers, underscores, hyphens, or periods.'
127+
)
128+
129+
124130
def code_field(**kwargs) -> MultiCollationCharField:
125131
"""
126132
Field to hold a 'code', i.e. a slug-like local identifier.
133+
134+
Use together with :func:`code_field_check` to enforce the same regex at
135+
the database level via a ``CheckConstraint``.
127136
"""
128137
return case_sensitive_char_field(
129138
max_length=255,
@@ -132,14 +141,37 @@ def code_field(**kwargs) -> MultiCollationCharField:
132141
RegexValidator(
133142
CODE_REGEX,
134143
# Translators: "letters" means latin letters: a-z and A-Z.
135-
_('Enter a valid "code name" consisting of letters, numbers, underscores, hyphens, or periods.'),
144+
_CODE_VIOLATION_MSG,
136145
"invalid",
137146
),
138147
],
139148
**kwargs,
140149
)
141150

142151

152+
def code_field_check(field_name: str, *, name: str) -> models.CheckConstraint:
153+
"""
154+
Return a ``CheckConstraint`` that enforces :data:`CODE_REGEX` at the DB level.
155+
156+
Django validators (used by :func:`code_field`) are not called on ``.save()``
157+
or ``.update()``. Adding this constraint ensures the regex is also enforced
158+
by the database itself, and Django will additionally run it as a Python-level
159+
validator automatically.
160+
161+
Usage::
162+
163+
class Meta:
164+
constraints = [
165+
code_field_check("my_code_field", name="myapp_mymodel_my_code_field_regex"),
166+
]
167+
"""
168+
return models.CheckConstraint(
169+
condition=Regex(models.F(field_name), CODE_REGEX.pattern),
170+
name=name,
171+
violation_error_message=_CODE_VIOLATION_MSG,
172+
)
173+
174+
143175
def key_field(**kwargs) -> MultiCollationCharField:
144176
"""
145177
Externally created Identifier fields.

0 commit comments

Comments
 (0)