Skip to content

Commit 428f22a

Browse files
committed
add more validations
1 parent 276f453 commit 428f22a

File tree

4 files changed

+93
-26
lines changed

4 files changed

+93
-26
lines changed

src/ol_openedx_course_sync/migrations/0001_initial.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.20 on 2025-05-13 08:28
1+
# Generated by Django 4.2.20 on 2025-05-14 09:11
22

33
import opaque_keys.edx.django.models
44
from django.db import migrations, models
@@ -24,7 +24,9 @@ class Migration(migrations.Migration):
2424
),
2525
(
2626
"source_course",
27-
opaque_keys.edx.django.models.CourseKeyField(max_length=255),
27+
opaque_keys.edx.django.models.CourseKeyField(
28+
max_length=255, unique=True
29+
),
2830
),
2931
(
3032
"target_courses",

src/ol_openedx_course_sync/models.py

+61-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Models for ol-openedx-course-sync plugin
33
"""
44

5+
from django.core.exceptions import ValidationError
56
from django.db import models
67
from opaque_keys.edx.django.models import (
78
CourseKeyField,
@@ -33,10 +34,69 @@ class CourseSyncMap(models.Model):
3334
courses that are part of any organization added in `CourseSyncParentOrg`.
3435
"""
3536

36-
source_course = CourseKeyField(max_length=255)
37+
source_course = CourseKeyField(max_length=255, unique=True)
3738
target_courses = models.TextField(
3839
blank=True, help_text="Comma separated list of target course keys"
3940
)
4041

4142
def __str__(self):
4243
return f"{self.source_course} Course Sync Map"
44+
45+
def save(self, *args, **kwargs):
46+
"""
47+
Override save method to perform custom validations.
48+
"""
49+
self.full_clean()
50+
super().save(*args, **kwargs)
51+
52+
def clean(self):
53+
"""
54+
Override clean method to perform custom validations.
55+
"""
56+
super().clean()
57+
58+
conflicting_targets = CourseSyncMap.objects.filter(
59+
target_courses__contains=self.source_course
60+
)
61+
if conflicting_targets:
62+
raise ValidationError(
63+
{
64+
"source_course": f"This course is already used as target course of: " # noqa: E501
65+
f"{', '.join(str(ct.source_course) for ct in conflicting_targets)}"
66+
}
67+
)
68+
69+
conflicting_sources = CourseSyncMap.objects.filter(
70+
source_course__in=self.target_course_keys
71+
)
72+
if conflicting_sources:
73+
raise ValidationError(
74+
{
75+
"target_courses": f"These course(s) are already used as source courses: " # noqa:E501
76+
f"{', '.join(str(cs.source_course) for cs in conflicting_sources)}"
77+
}
78+
)
79+
80+
if self.target_course_keys:
81+
query = models.Q()
82+
for key in self.target_course_keys:
83+
query |= models.Q(**{"target_courses__contains": key})
84+
duplicate_targets = CourseSyncMap.objects.filter(query)
85+
86+
if self.pk:
87+
duplicate_targets = duplicate_targets.exclude(pk=self.pk)
88+
89+
if duplicate_targets:
90+
raise ValidationError(
91+
{
92+
"target_courses": f"Some of these course(s) are already used as target course(s) for: " # noqa:E501
93+
f"{', '.join(str(dt.source_course) for dt in duplicate_targets)}" # noqa:E501
94+
}
95+
)
96+
97+
@property
98+
def target_course_keys(self):
99+
"""
100+
Returns a list of target course keys.
101+
"""
102+
return [key for key in self.target_courses.strip().split(",") if key]

src/ol_openedx_course_sync/signals.py

+28-23
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66

77
from common.djangoapps.course_action_state.models import CourseRerunState
8+
from django.core.exceptions import ValidationError
89
from django.db.models.signals import post_save
910
from django.dispatch import receiver
1011
from ol_openedx_course_sync.constants import COURSE_RERUN_STATE_SUCCEEDED
@@ -29,12 +30,15 @@ def listen_for_course_publish(
2930

3031
course_sync_map = CourseSyncMap.objects.filter(source_course=course_key).first()
3132
if not (course_sync_map and course_sync_map.target_courses):
32-
log.info("No mapping found for course %s. Skipping copy.", course_key.id)
33+
log.info("No mapping found for course %s. Skipping copy.", str(course_key))
3334
return
3435

3536
source_course = str(course_sync_map.source_course)
3637
user_id = None
37-
for target_course_key in course_sync_map.target_courses.split(","):
38+
target_keys = [
39+
key for key in course_sync_map.target_courses.strip().split(",") if key
40+
]
41+
for target_course_key in target_keys:
3842
log.info(
3943
"Initializing async course content sync from %s to %s",
4044
source_course,
@@ -57,30 +61,31 @@ def listen_for_course_rerun_state_post_save(sender, instance, **kwargs): # noqa
5761
).exists():
5862
return
5963

60-
# If source course is a target/child of any other course, we won't make it
61-
# a source/parent to avoid loops in course sync
62-
if CourseSyncMap.objects.filter(
63-
target_courses__contains=str(instance.source_course_key)
64-
).exists():
65-
log.warning(
66-
"Course %s is already a target course. Skipping.",
64+
try:
65+
course_sync_map, _ = CourseSyncMap.objects.get_or_create(
66+
source_course=instance.source_course_key
67+
)
68+
except ValidationError:
69+
log.exception(
70+
"Failed to create CourseSyncMap for %s",
6771
instance.source_course_key,
6872
)
6973
return
7074

71-
course_sync_map, _ = CourseSyncMap.objects.get_or_create(
72-
source_course=instance.source_course_key
73-
)
74-
target_courses = (
75-
course_sync_map.target_courses.split(",")
76-
if course_sync_map.target_courses
77-
else []
78-
)
75+
target_courses = course_sync_map.target_course_keys
7976
target_courses.append(str(instance.course_key))
8077
course_sync_map.target_courses = ",".join(target_courses)
81-
course_sync_map.save()
82-
log.info(
83-
"Added course %s to target courses for %s",
84-
instance.course_key,
85-
instance.source_course_key,
86-
)
78+
79+
try:
80+
course_sync_map.save()
81+
except ValidationError:
82+
log.exception(
83+
"Failed to update CourseSyncMap for %s",
84+
instance.source_course_key,
85+
)
86+
else:
87+
log.info(
88+
"Added course %s to target courses for %s",
89+
instance.course_key,
90+
instance.source_course_key,
91+
)

src/ol_openedx_course_sync/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)