|
7 | 7 | from django.contrib import admin |
8 | 8 | from django.core.exceptions import ValidationError |
9 | 9 | from django.db import models |
10 | | -from django.db.models.functions import Concat, Length, Lower, Right |
11 | | -from django.db.models.lookups import Exact |
| 10 | +from django.db.models import F |
| 11 | +from django.db.models.functions import Length, Lower, StrIndex |
| 12 | +from django.db.models.lookups import GreaterThan |
12 | 13 | from django.utils.translation import gettext_lazy as _ |
13 | 14 | from opaque_keys import InvalidKeyError |
14 | 15 | from opaque_keys.edx.django.models import CourseKeyField |
@@ -214,19 +215,26 @@ class Meta: |
214 | 215 | ordering = ("-created",) |
215 | 216 | constraints = [ |
216 | 217 | # catalog_course (org+course_code) and run must be unique together: |
217 | | - models.UniqueConstraint("catalog_course", "run", name="oex_catalog_courserun_catalog_course_run_uniq"), |
| 218 | + models.UniqueConstraint( |
| 219 | + "catalog_course", |
| 220 | + "run", |
| 221 | + name="oex_catalog_courserun_catalog_course_run_uniq", |
| 222 | + # With one unfortunate exception: CCX courses all have the same org, code, and run exactly: |
| 223 | + condition=~models.Q(course_id__startswith="ccx"), |
| 224 | + ), |
218 | 225 | # course_id is case-sensitively unique but we also want it to be case-insensitively unique: |
219 | 226 | models.UniqueConstraint(Lower("course_id"), name="oex_catalog_courserun_course_id_ci"), |
220 | 227 | # Enforce at the DB level that these required fields are not blank: |
221 | 228 | models.CheckConstraint(condition=models.Q(run__length__gt=0), name="oex_catalog_courserun_run_not_blank"), |
222 | 229 | models.CheckConstraint( |
223 | 230 | condition=models.Q(display_name__length__gt=0), name="oex_catalog_courserun_display_name_not_blank" |
224 | 231 | ), |
225 | | - # Enforce that the course ID must end with "+run" where "run" is an exact match for the "run" field. |
226 | | - # This check may be removed or changed in the future if our course ID format ever changes |
| 232 | + # Enforce at the DB level that the "run" field value appears in the course ID: |
227 | 233 | models.CheckConstraint( |
228 | | - # Note: EndsWith() on SQLite is always case-insensitive, so we code the constraint like this: |
229 | | - condition=Exact(Right("course_id", Length("run") + 1), Concat(models.Value("+"), "run")), |
| 234 | + condition=GreaterThan(StrIndex("course_id", F("run")), 0), |
| 235 | + # The following check condition (ends with "+run") is even stronger, but doesn't work with CCX keys |
| 236 | + # like "ccx-v1:org+code+run+ccx@1" which we also need to support. |
| 237 | + # condition=Exact(Right("course_id", Length("run") + 1), Concat(models.Value("+"), "run")), |
230 | 238 | name="oex_catalog_courserun_courseid_run_match_exactly", |
231 | 239 | violation_error_message=_("The CourseRun 'run' field should match the run in the course_id key."), |
232 | 240 | ), |
|
0 commit comments