4444from contentcuration import models as ccmodels
4545from contentcuration .decorators import delay_user_storage_calculation
4646from contentcuration .utils .assessment .perseus import PerseusExerciseGenerator
47+ from contentcuration .utils .assessment .qti .archive import QTIExerciseGenerator
48+ from contentcuration .utils .assessment .qti .imsmanifest import (
49+ get_assessment_ids_from_manifest ,
50+ )
4751from contentcuration .utils .cache import delete_public_channel_cache_keys
4852from contentcuration .utils .files import create_thumbnail_from_base64
4953from contentcuration .utils .files import get_thumbnail_encoding
@@ -319,20 +323,43 @@ def recurse_nodes(self, node, inherited_fields): # noqa C901
319323 )
320324
321325 if node .kind_id == content_kinds .EXERCISE :
322- exercise_data = process_assessment_metadata (node , kolibrinode )
326+ exercise_data = process_assessment_metadata (node )
327+ any_free_response = any (
328+ t == exercises .FREE_RESPONSE
329+ for t in exercise_data ["assessment_mapping" ].values ()
330+ )
331+ generator_class = (
332+ QTIExerciseGenerator
333+ if any_free_response
334+ else PerseusExerciseGenerator
335+ )
336+
337+ # If this exercise previously had a file generated by a different
338+ # generator, make sure we clean it up here.
339+ stale_presets = {
340+ PerseusExerciseGenerator .preset ,
341+ QTIExerciseGenerator .preset ,
342+ } - {generator_class .preset }
343+
344+ # Remove archives produced by the previously-used generator
345+ node .files .filter (preset_id__in = stale_presets ).delete ()
346+
323347 if (
324348 self .force_exercises
325349 or node .changed
326- or not node .files .filter (preset_id = format_presets . EXERCISE ).exists ()
350+ or not node .files .filter (preset_id = generator_class . preset ).exists ()
327351 ):
328- generator = PerseusExerciseGenerator (
352+
353+ generator = generator_class (
329354 node ,
330355 exercise_data ,
331356 self .channel_id ,
332357 self .default_language .lang_code ,
333358 user_id = self .user_id ,
334359 )
335360 generator .create_exercise_archive ()
361+
362+ create_kolibri_assessment_metadata (node , kolibrinode )
336363 elif node .kind_id == content_kinds .SLIDESHOW :
337364 create_slideshow_manifest (node , user_id = self .user_id )
338365 elif node .kind_id == content_kinds .TOPIC :
@@ -625,11 +652,7 @@ def parse_assessment_metadata(ccnode):
625652 )
626653
627654
628- def process_assessment_metadata (ccnode , kolibrinode ):
629- # Get mastery model information, set to default if none provided
630- assessment_items = ccnode .assessment_items .all ().order_by ("order" )
631- assessment_item_ids = [a .assessment_id for a in assessment_items ]
632-
655+ def _get_exercise_data_from_ccnode (ccnode , num_assessment_items ):
633656 randomize , mastery_criteria = parse_assessment_metadata (ccnode )
634657
635658 exercise_data = deepcopy (mastery_criteria )
@@ -638,14 +661,14 @@ def process_assessment_metadata(ccnode, kolibrinode):
638661 mastery_model = {"type" : exercise_data_type or exercises .M_OF_N }
639662 if mastery_model ["type" ] == exercises .M_OF_N :
640663 mastery_model .update (
641- {"n" : exercise_data .get ("n" ) or min (5 , assessment_items . count () ) or 1 }
664+ {"n" : exercise_data .get ("n" ) or min (5 , num_assessment_items ) or 1 }
642665 )
643666 mastery_model .update (
644- {"m" : exercise_data .get ("m" ) or min (5 , assessment_items . count () ) or 1 }
667+ {"m" : exercise_data .get ("m" ) or min (5 , num_assessment_items ) or 1 }
645668 )
646669 elif mastery_model ["type" ] == exercises .DO_ALL :
647670 mastery_model .update (
648- {"n" : assessment_items . count () or 1 , "m" : assessment_items . count () or 1 }
671+ {"n" : num_assessment_items or 1 , "m" : num_assessment_items or 1 }
649672 )
650673 elif mastery_model ["type" ] == exercises .NUM_CORRECT_IN_A_ROW_2 :
651674 mastery_model .update ({"n" : 2 , "m" : 2 })
@@ -655,6 +678,17 @@ def process_assessment_metadata(ccnode, kolibrinode):
655678 mastery_model .update ({"n" : 5 , "m" : 5 })
656679 elif mastery_model ["type" ] == exercises .NUM_CORRECT_IN_A_ROW_10 :
657680 mastery_model .update ({"n" : 10 , "m" : 10 })
681+ return randomize , exercise_data , mastery_model
682+
683+
684+ def process_assessment_metadata (ccnode ):
685+ # Get mastery model information, set to default if none provided
686+ assessment_items = ccnode .assessment_items .all ().order_by ("order" )
687+ assessment_item_ids = [a .assessment_id for a in assessment_items ]
688+
689+ randomize , exercise_data , mastery_model = _get_exercise_data_from_ccnode (
690+ ccnode , len (assessment_item_ids )
691+ )
658692
659693 exercise_data .update (
660694 {
@@ -673,18 +707,31 @@ def process_assessment_metadata(ccnode, kolibrinode):
673707 }
674708 )
675709
710+ return exercise_data
711+
712+
713+ def create_kolibri_assessment_metadata (ccnode , kolibrinode ):
714+ assessment_items = ccnode .assessment_items .all ().order_by ("order" )
715+ assessment_item_ids = [a .assessment_id for a in assessment_items ]
716+ randomize , _ , mastery_model = _get_exercise_data_from_ccnode (
717+ ccnode , len (assessment_item_ids )
718+ )
719+ qti_file = ccnode .files .filter (preset_id = format_presets .QTI_ZIP ).first ()
720+ if qti_file :
721+ # Open the zip file from Django storage
722+ with qti_file .file_on_disk .open ("rb" ) as file_handle :
723+ assessment_item_ids = get_assessment_ids_from_manifest (file_handle )
724+
676725 kolibrimodels .AssessmentMetaData .objects .create (
677726 id = uuid .uuid4 (),
678727 contentnode = kolibrinode ,
679728 assessment_item_ids = assessment_item_ids ,
680- number_of_assessments = assessment_items . count ( ),
729+ number_of_assessments = len ( assessment_item_ids ),
681730 mastery_model = mastery_model ,
682731 randomize = randomize ,
683732 is_manipulable = ccnode .kind_id == content_kinds .EXERCISE ,
684733 )
685734
686- return exercise_data
687-
688735
689736def map_prerequisites (root_node ):
690737
0 commit comments