From b2984a0c9bdf50c4e6cbe437aaf8b2fade58b45d Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Mon, 2 Mar 2026 16:14:27 -0800 Subject: [PATCH] Add additional completion to exercises that have already had their completion criteria fixed but their complete value is still wrong --- Makefile | 2 +- .../commands/fix_exercise_extra_fields.py | 20 +++++++++++++- .../tests/test_contentnodes.py | 26 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index dc1e70b51e..3f164c5c26 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ migrate: # 4) Remove the management command from this `deploy-migrate` recipe # 5) Repeat! deploy-migrate: - echo "Nothing to do here!" + python contentcuration/manage.py fix_exercise_extra_fields contentnodegc: python contentcuration/manage.py garbage_collect diff --git a/contentcuration/contentcuration/management/commands/fix_exercise_extra_fields.py b/contentcuration/contentcuration/management/commands/fix_exercise_extra_fields.py index 2bf50a93c5..97142b60ab 100644 --- a/contentcuration/contentcuration/management/commands/fix_exercise_extra_fields.py +++ b/contentcuration/contentcuration/management/commands/fix_exercise_extra_fields.py @@ -65,6 +65,7 @@ def handle(self, *args, **options): migrated_complete = 0 old_style_fixed = 0 old_style_complete = 0 + incomplete_fixed = 0 exercises_checked = 0 for node in queryset.iterator(chunk_size=CHUNKSIZE): @@ -77,6 +78,8 @@ def handle(self, *args, **options): migrated_fixed += 1 if complete: migrated_complete += 1 + elif fix_type == "incomplete" and complete: + incomplete_fixed += 1 exercises_checked += 1 if exercises_checked % CHUNKSIZE == 0: logging.info( @@ -92,6 +95,11 @@ def handle(self, *args, **options): migrated_complete, migrated_fixed ) ) + logging.info( + "{} marked complete that were previously incomplete".format( + incomplete_fixed + ) + ) logging.info("{} / {} exercises checked".format(exercises_checked, total)) logging.info( @@ -104,18 +112,26 @@ def handle(self, *args, **options): migrated_complete, migrated_fixed ) ) + logging.info( + "{} marked complete that were previously incomplete".format( + incomplete_fixed + ) + ) logging.info( "Done in {:.1f}s. Fixed {} migrated exercises, " - "migrated {} old-style exercises.{}".format( + "migrated {} old-style exercises." + "marked {} previously incomplete exercises complete. {}".format( time.time() - start, migrated_fixed, old_style_fixed, + incomplete_fixed, " (dry run)" if dry_run else "", ) ) def _process_node(self, node, dry_run): ef = node.extra_fields + was_complete = node.complete if isinstance(ef, str): try: ef = json.loads(ef) @@ -131,6 +147,8 @@ def _process_node(self, node, dry_run): ef["options"]["completion_criteria"]["threshold"]["m"] = None ef["options"]["completion_criteria"]["threshold"]["n"] = None fix_type = "m_n_fix" + elif not was_complete: + fix_type = "incomplete" else: return None, None node.extra_fields = ef diff --git a/contentcuration/contentcuration/tests/test_contentnodes.py b/contentcuration/contentcuration/tests/test_contentnodes.py index c1d8fce07a..0de23ab008 100644 --- a/contentcuration/contentcuration/tests/test_contentnodes.py +++ b/contentcuration/contentcuration/tests/test_contentnodes.py @@ -1817,6 +1817,32 @@ def test_dry_run_does_not_modify(self): self.assertEqual(threshold["m"], 0) self.assertEqual(threshold["n"], 0) + def test_incomplete_node_with_valid_fields_gets_marked_complete(self): + """An incomplete exercise with valid extra_fields should be marked complete.""" + node = self._create_exercise( + { + "options": { + "completion_criteria": { + "threshold": { + "mastery_model": exercises.DO_ALL, + "m": None, + "n": None, + }, + "model": completion_criteria.MASTERY, + } + }, + } + ) + # Force incomplete status even though fields are valid + node.complete = False + node.save() + + command = FixExerciseExtraFieldsCommand() + command.handle() + + node.refresh_from_db() + self.assertTrue(node.complete) + def test_migrates_string_extra_fields(self): """Command should parse and migrate extra_fields stored as a JSON string.""" node = self._create_exercise(