Skip to content

Commit 4854369

Browse files
committed
Update mark_complete to handle completion criteria for non-exercises.
Add verification when a contentnode is marked complete via the API.
1 parent c8f1969 commit 4854369

4 files changed

Lines changed: 75 additions & 2 deletions

File tree

contentcuration/contentcuration/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,6 +1846,13 @@ def mark_complete(self): # noqa C901
18461846
completion_criteria.validate(criterion, kind=content_kinds.EXERCISE)
18471847
except completion_criteria.ValidationError:
18481848
errors.append("Mastery criterion is defined but is invalid")
1849+
else:
1850+
criterion = self.extra_fields.get("options", {}).get("completion_criteria", {})
1851+
if criterion:
1852+
try:
1853+
completion_criteria.validate(criterion, kind=self.kind_id)
1854+
except completion_criteria.ValidationError:
1855+
errors.append("Completion criterion is defined but is invalid")
18491856
self.complete = not errors
18501857
return errors
18511858

contentcuration/contentcuration/tests/test_contentnodes.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,33 @@ def test_create_video_thumbnail_only(self):
10401040
new_obj.mark_complete()
10411041
self.assertFalse(new_obj.complete)
10421042

1043+
def test_create_video_invalid_completion_criterion(self):
1044+
licenses = list(License.objects.filter(copyright_holder_required=False, is_custom=False).values_list("pk", flat=True))
1045+
channel = testdata.channel()
1046+
new_obj = ContentNode(
1047+
title="yes",
1048+
kind_id=content_kinds.VIDEO,
1049+
parent=channel.main_tree,
1050+
license_id=licenses[0],
1051+
copyright_holder="Some person",
1052+
extra_fields={
1053+
"randomize": False,
1054+
"options": {
1055+
"completion_criteria": {
1056+
"threshold": {
1057+
"mastery_model": exercises.M_OF_N,
1058+
"n": 5,
1059+
},
1060+
"model": completion_criteria.MASTERY,
1061+
}
1062+
}
1063+
},
1064+
)
1065+
new_obj.save()
1066+
File.objects.create(contentnode=new_obj, preset_id=format_presets.VIDEO_HIGH_RES, checksum=uuid.uuid4().hex)
1067+
new_obj.mark_complete()
1068+
self.assertFalse(new_obj.complete)
1069+
10431070
def test_create_exercise_no_assessment_items(self):
10441071
licenses = list(License.objects.filter(copyright_holder_required=False, is_custom=False).values_list("pk", flat=True))
10451072
channel = testdata.channel()

contentcuration/contentcuration/tests/viewsets/test_contentnode.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from contentcuration.viewsets.contentnode import ContentNodeFilter
3434
from contentcuration.viewsets.sync.constants import CONTENTNODE
3535
from contentcuration.viewsets.sync.constants import CONTENTNODE_PREREQUISITE
36+
from contentcuration.viewsets.sync.constants import UPDATED
3637

3738

3839
nested_subjects = [subject for subject in SUBJECTSLIST if "." in subject]
@@ -661,6 +662,24 @@ def test_update_contentnode_exercise_mastery_model_old(self):
661662
models.ContentNode.objects.get(id=contentnode.id).extra_fields["options"]["completion_criteria"]["model"], completion_criteria.MASTERY
662663
)
663664

665+
def test_update_contentnode_exercise_incomplete_mastery_model_marked_complete(self):
666+
metadata = self.contentnode_db_metadata
667+
metadata["kind_id"] = content_kinds.EXERCISE
668+
contentnode = models.ContentNode.objects.create(**metadata)
669+
670+
response = self.sync_changes(
671+
[generate_update_event(contentnode.id, CONTENTNODE, {
672+
"complete": True,
673+
}, channel_id=self.channel.id)],
674+
)
675+
676+
self.assertEqual(response.status_code, 200, response.content)
677+
self.assertFalse(
678+
models.ContentNode.objects.get(id=contentnode.id).complete
679+
)
680+
change = models.Change.objects.filter(channel=self.channel, change_type=UPDATED, table=CONTENTNODE).last()
681+
self.assertFalse(change.kwargs["mods"]["complete"])
682+
664683
def test_update_contentnode_extra_fields(self):
665684
contentnode = models.ContentNode.objects.create(**self.contentnode_db_metadata)
666685
# Update extra_fields.randomize

contentcuration/contentcuration/viewsets/contentnode.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
from django.http import Http404
1717
from django.utils.timezone import now
1818
from django_cte import CTEQuerySet
19+
from django_filters.rest_framework import BooleanFilter
1920
from django_filters.rest_framework import CharFilter
2021
from django_filters.rest_framework import UUIDFilter
21-
from django_filters.rest_framework import BooleanFilter
2222
from le_utils.constants import completion_criteria
2323
from le_utils.constants import content_kinds
2424
from le_utils.constants import roles
@@ -412,6 +412,21 @@ def _check_completion_criteria(self, kind, complete, validated_data):
412412
except DjangoValidationError as e:
413413
raise ValidationError(e)
414414

415+
def _ensure_complete(self, instance):
416+
"""
417+
If an instance is marked as complete, ensure that it is actually complete.
418+
If it is not, update the value, save, and issue a change event.
419+
"""
420+
if instance.complete:
421+
instance.mark_complete()
422+
if not instance.complete:
423+
instance.save()
424+
Change.create_change(
425+
generate_update_event(
426+
instance.id, CONTENTNODE, {"complete": False}, channel_id=instance.channel_id
427+
), created_by_id=self.context["request"].user.id, applied=True
428+
)
429+
415430
def create(self, validated_data):
416431
tags = None
417432
if "tags" in validated_data:
@@ -424,6 +439,8 @@ def create(self, validated_data):
424439
if tags:
425440
set_tags({instance.id: tags})
426441

442+
self._ensure_complete(instance)
443+
427444
return instance
428445

429446
def update(self, instance, validated_data):
@@ -439,7 +456,10 @@ def update(self, instance, validated_data):
439456

440457
self._check_completion_criteria(validated_data.get("kind", instance.kind_id), validated_data.get("complete", instance.complete), validated_data)
441458

442-
return super(ContentNodeSerializer, self).update(instance, validated_data)
459+
instance = super(ContentNodeSerializer, self).update(instance, validated_data)
460+
461+
self._ensure_complete(instance)
462+
return instance
443463

444464

445465
def retrieve_thumbail_src(item):

0 commit comments

Comments
 (0)