diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js index 8452cc0641..802e46c233 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js @@ -3,18 +3,33 @@ import { isNodeComplete } from 'shared/utils/validation'; import db from 'shared/data/db'; import { TABLE_NAMES } from 'shared/data/constants'; -function updateNodeComplete(nodeId, context) { - const node = context.rootGetters['contentNode/getContentNode'](nodeId); - const complete = isNodeComplete({ - nodeDetails: node, - assessmentItems: context.getters.getAssessmentItems(nodeId), - files: context.rootGetters['file/getContentNodeFiles'](nodeId), - }); - return context.dispatch( - 'contentNode/updateContentNode', - { id: nodeId, complete }, - { root: true } - ); +// We implement a retry mechanism to ensure that we wait for retrival of contentnode +// when all the nodes for the +// currently displayed topic in the tree view are reloaded +function updateNodeComplete(nodeId, context, maxTries = 10, delayMs = 100) { + let tries = 0; + + function tryUpdate() { + const node = context.rootGetters['contentNode/getContentNode'](nodeId); + if (node) { + const complete = isNodeComplete({ + nodeDetails: node, + assessmentItems: context.getters.getAssessmentItems(nodeId), + files: context.rootGetters['file/getContentNodeFiles'](nodeId), + }); + return context.dispatch( + 'contentNode/updateContentNode', + { id: nodeId, complete }, + { root: true } + ); + } else if (tries < maxTries) { + tries++; + setTimeout(tryUpdate, delayMs); + } else { + console.error(`updateNodeComplete: Node ${nodeId} not found in Vuex after ${maxTries} tries`); + } + } + tryUpdate(); } /** diff --git a/contentcuration/contentcuration/migrations/0151_alter_assessmentitem_type.py b/contentcuration/contentcuration/migrations/0151_alter_assessmentitem_type.py deleted file mode 100644 index eb13f2909f..0000000000 --- a/contentcuration/contentcuration/migrations/0151_alter_assessmentitem_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.24 on 2025-01-31 03:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contentcuration', '0150_bloompub_format_and_preset'), - ] - - operations = [ - migrations.AlterField( - model_name='assessmentitem', - name='type', - field=models.CharField(choices=[('input_question', 'Input Question'), ('multiple_selection', 'Multiple Selection'), ('single_selection', 'Single Selection'), ('free_response', 'Free Response'), ('perseus_question', 'Perseus Question')], default='multiple_selection', max_length=50), - ), - ] diff --git a/contentcuration/contentcuration/migrations/0151_auto_20250417_1516.py b/contentcuration/contentcuration/migrations/0151_auto_20250417_1516.py new file mode 100644 index 0000000000..d17bd8eaa5 --- /dev/null +++ b/contentcuration/contentcuration/migrations/0151_auto_20250417_1516.py @@ -0,0 +1,87 @@ +# Generated by Django 3.2.24 on 2025-04-17 15:16 +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contentcuration", "0150_bloompub_format_and_preset"), + ] + + operations = [ + migrations.AlterField( + model_name="fileformat", + name="extension", + field=models.CharField( + choices=[ + ("mp4", "MP4 Video"), + ("webm", "WEBM Video"), + ("vtt", "VTT Subtitle"), + ("mp3", "MP3 Audio"), + ("pdf", "PDF Document"), + ("jpg", "JPG Image"), + ("jpeg", "JPEG Image"), + ("png", "PNG Image"), + ("gif", "GIF Image"), + ("json", "JSON"), + ("svg", "SVG Image"), + ("perseus", "Perseus Exercise"), + ("graphie", "Graphie Exercise"), + ("zip", "HTML5 Zip"), + ("h5p", "H5P"), + ("zim", "ZIM"), + ("epub", "ePub Document"), + ("bloompub", "Bloom Document"), + ("bloomd", "Bloom Document"), + ("kpub", "Kolibri HTML5 Article"), + ], + max_length=40, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="formatpreset", + name="id", + field=models.CharField( + choices=[ + ("high_res_video", "High Resolution"), + ("low_res_video", "Low Resolution"), + ("video_thumbnail", "Thumbnail"), + ("video_subtitle", "Subtitle"), + ("video_dependency", "Video (dependency)"), + ("audio", "Audio"), + ("audio_thumbnail", "Thumbnail"), + ("audio_dependency", "audio (dependency)"), + ("document", "Document"), + ("epub", "ePub Document"), + ("document_thumbnail", "Thumbnail"), + ("exercise", "Exercise"), + ("exercise_thumbnail", "Thumbnail"), + ("exercise_image", "Exercise Image"), + ("exercise_graphie", "Exercise Graphie"), + ("channel_thumbnail", "Channel Thumbnail"), + ("topic_thumbnail", "Thumbnail"), + ("html5_zip", "HTML5 Zip"), + ("html5_dependency", "HTML5 Dependency (Zip format)"), + ("html5_thumbnail", "HTML5 Thumbnail"), + ("h5p", "H5P Zip"), + ("h5p_thumbnail", "H5P Thumbnail"), + ("zim", "Zim"), + ("zim_thumbnail", "Zim Thumbnail"), + ("qti", "QTI Zip"), + ("qti_thumbnail", "QTI Thumbnail"), + ("slideshow_image", "Slideshow Image"), + ("slideshow_thumbnail", "Slideshow Thumbnail"), + ("slideshow_manifest", "Slideshow Manifest"), + ("imscp_zip", "IMSCP Zip"), + ("bloompub", "Bloom Document"), + ("kpub", "Kolibri HTML5 Article"), + ], + max_length=150, + primary_key=True, + serialize=False, + ), + ), + ] diff --git a/contentcuration/contentcuration/migrations/0152_alter_assessmentitem_type.py b/contentcuration/contentcuration/migrations/0152_alter_assessmentitem_type.py new file mode 100644 index 0000000000..f047648bb3 --- /dev/null +++ b/contentcuration/contentcuration/migrations/0152_alter_assessmentitem_type.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.24 on 2025-04-17 16:09 +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contentcuration", "0151_auto_20250417_1516"), + ] + + operations = [ + migrations.AlterField( + model_name="assessmentitem", + name="type", + field=models.CharField( + choices=[ + ("input_question", "Input Question"), + ("multiple_selection", "Multiple Selection"), + ("single_selection", "Single Selection"), + ("free_response", "Free Response"), + ("perseus_question", "Perseus Question"), + ], + default="multiple_selection", + max_length=50, + ), + ), + ] diff --git a/contentcuration/contentcuration/viewsets/contentnode.py b/contentcuration/contentcuration/viewsets/contentnode.py index 772790e5f2..49f2cf3e35 100644 --- a/contentcuration/contentcuration/viewsets/contentnode.py +++ b/contentcuration/contentcuration/viewsets/contentnode.py @@ -288,7 +288,7 @@ def update(self, instance, validated_data): class ExtraFieldsOptionsSerializer(JSONFieldDictSerializer): - modality = ChoiceField(choices=(("QUIZ", "Quiz"),("SURVEY","Survey")), allow_null=True, required=False) + modality = ChoiceField(choices=(("QUIZ", "Quiz"), ("SURVEY", "Survey")), allow_null=True, required=False) completion_criteria = CompletionCriteriaSerializer(required=False) @@ -689,6 +689,11 @@ def decode_cursor(self, request): if value is None: return None + try: + value = int(value) + except ValueError: + raise ValidationError("lft must be an integer but an invalid value was given.") + return Cursor(offset=0, reverse=False, position=value) def encode_cursor(self, cursor): diff --git a/contentcuration/kolibri_content/migrations/0023_auto_20250417_1516.py b/contentcuration/kolibri_content/migrations/0023_auto_20250417_1516.py new file mode 100644 index 0000000000..daac7180fc --- /dev/null +++ b/contentcuration/kolibri_content/migrations/0023_auto_20250417_1516.py @@ -0,0 +1,115 @@ +# Generated by Django 3.2.24 on 2025-04-17 15:16 +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0022_auto_20240915_1414"), + ] + + operations = [ + migrations.AlterField( + model_name="file", + name="extension", + field=models.CharField( + blank=True, + choices=[ + ("mp4", "MP4 Video"), + ("webm", "WEBM Video"), + ("vtt", "VTT Subtitle"), + ("mp3", "MP3 Audio"), + ("pdf", "PDF Document"), + ("jpg", "JPG Image"), + ("jpeg", "JPEG Image"), + ("png", "PNG Image"), + ("gif", "GIF Image"), + ("json", "JSON"), + ("svg", "SVG Image"), + ("perseus", "Perseus Exercise"), + ("graphie", "Graphie Exercise"), + ("zip", "HTML5 Zip"), + ("h5p", "H5P"), + ("zim", "ZIM"), + ("epub", "ePub Document"), + ("bloompub", "Bloom Document"), + ("bloomd", "Bloom Document"), + ("kpub", "Kolibri HTML5 Article"), + ], + max_length=40, + ), + ), + migrations.AlterField( + model_name="file", + name="preset", + field=models.CharField( + blank=True, + choices=[ + ("high_res_video", "High Resolution"), + ("low_res_video", "Low Resolution"), + ("video_thumbnail", "Thumbnail"), + ("video_subtitle", "Subtitle"), + ("video_dependency", "Video (dependency)"), + ("audio", "Audio"), + ("audio_thumbnail", "Thumbnail"), + ("audio_dependency", "audio (dependency)"), + ("document", "Document"), + ("epub", "ePub Document"), + ("document_thumbnail", "Thumbnail"), + ("exercise", "Exercise"), + ("exercise_thumbnail", "Thumbnail"), + ("exercise_image", "Exercise Image"), + ("exercise_graphie", "Exercise Graphie"), + ("channel_thumbnail", "Channel Thumbnail"), + ("topic_thumbnail", "Thumbnail"), + ("html5_zip", "HTML5 Zip"), + ("html5_dependency", "HTML5 Dependency (Zip format)"), + ("html5_thumbnail", "HTML5 Thumbnail"), + ("h5p", "H5P Zip"), + ("h5p_thumbnail", "H5P Thumbnail"), + ("zim", "Zim"), + ("zim_thumbnail", "Zim Thumbnail"), + ("qti", "QTI Zip"), + ("qti_thumbnail", "QTI Thumbnail"), + ("slideshow_image", "Slideshow Image"), + ("slideshow_thumbnail", "Slideshow Thumbnail"), + ("slideshow_manifest", "Slideshow Manifest"), + ("imscp_zip", "IMSCP Zip"), + ("bloompub", "Bloom Document"), + ("kpub", "Kolibri HTML5 Article"), + ], + max_length=150, + ), + ), + migrations.AlterField( + model_name="localfile", + name="extension", + field=models.CharField( + blank=True, + choices=[ + ("mp4", "MP4 Video"), + ("webm", "WEBM Video"), + ("vtt", "VTT Subtitle"), + ("mp3", "MP3 Audio"), + ("pdf", "PDF Document"), + ("jpg", "JPG Image"), + ("jpeg", "JPEG Image"), + ("png", "PNG Image"), + ("gif", "GIF Image"), + ("json", "JSON"), + ("svg", "SVG Image"), + ("perseus", "Perseus Exercise"), + ("graphie", "Graphie Exercise"), + ("zip", "HTML5 Zip"), + ("h5p", "H5P"), + ("zim", "ZIM"), + ("epub", "ePub Document"), + ("bloompub", "Bloom Document"), + ("bloomd", "Bloom Document"), + ("kpub", "Kolibri HTML5 Article"), + ], + max_length=40, + ), + ), + ] diff --git a/contentcuration/kolibri_public/migrations/0006_auto_20250417_1516.py b/contentcuration/kolibri_public/migrations/0006_auto_20250417_1516.py new file mode 100644 index 0000000000..d9f798e9b9 --- /dev/null +++ b/contentcuration/kolibri_public/migrations/0006_auto_20250417_1516.py @@ -0,0 +1,85 @@ +# Generated by Django 3.2.24 on 2025-04-17 15:16 +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("kolibri_public", "0005_alter_localfile_extension"), + ] + + operations = [ + migrations.AlterField( + model_name="file", + name="preset", + field=models.CharField( + blank=True, + choices=[ + ("high_res_video", "High Resolution"), + ("low_res_video", "Low Resolution"), + ("video_thumbnail", "Thumbnail"), + ("video_subtitle", "Subtitle"), + ("video_dependency", "Video (dependency)"), + ("audio", "Audio"), + ("audio_thumbnail", "Thumbnail"), + ("audio_dependency", "audio (dependency)"), + ("document", "Document"), + ("epub", "ePub Document"), + ("document_thumbnail", "Thumbnail"), + ("exercise", "Exercise"), + ("exercise_thumbnail", "Thumbnail"), + ("exercise_image", "Exercise Image"), + ("exercise_graphie", "Exercise Graphie"), + ("channel_thumbnail", "Channel Thumbnail"), + ("topic_thumbnail", "Thumbnail"), + ("html5_zip", "HTML5 Zip"), + ("html5_dependency", "HTML5 Dependency (Zip format)"), + ("html5_thumbnail", "HTML5 Thumbnail"), + ("h5p", "H5P Zip"), + ("h5p_thumbnail", "H5P Thumbnail"), + ("zim", "Zim"), + ("zim_thumbnail", "Zim Thumbnail"), + ("qti", "QTI Zip"), + ("qti_thumbnail", "QTI Thumbnail"), + ("slideshow_image", "Slideshow Image"), + ("slideshow_thumbnail", "Slideshow Thumbnail"), + ("slideshow_manifest", "Slideshow Manifest"), + ("imscp_zip", "IMSCP Zip"), + ("bloompub", "Bloom Document"), + ("kpub", "Kolibri HTML5 Article"), + ], + max_length=150, + ), + ), + migrations.AlterField( + model_name="localfile", + name="extension", + field=models.CharField( + blank=True, + choices=[ + ("mp4", "MP4 Video"), + ("webm", "WEBM Video"), + ("vtt", "VTT Subtitle"), + ("mp3", "MP3 Audio"), + ("pdf", "PDF Document"), + ("jpg", "JPG Image"), + ("jpeg", "JPEG Image"), + ("png", "PNG Image"), + ("gif", "GIF Image"), + ("json", "JSON"), + ("svg", "SVG Image"), + ("perseus", "Perseus Exercise"), + ("graphie", "Graphie Exercise"), + ("zip", "HTML5 Zip"), + ("h5p", "H5P"), + ("zim", "ZIM"), + ("epub", "ePub Document"), + ("bloompub", "Bloom Document"), + ("bloomd", "Bloom Document"), + ("kpub", "Kolibri HTML5 Article"), + ], + max_length=40, + ), + ), + ] diff --git a/requirements.in b/requirements.in index a626e75967..c03fe67455 100644 --- a/requirements.in +++ b/requirements.in @@ -5,7 +5,7 @@ djangorestframework==3.15.1 psycopg2-binary==2.9.10 django-js-reverse==0.10.2 django-registration==3.4 -le-utils==0.2.9 +le-utils==0.2.10 gunicorn==23.0.0 django-postmark==0.1.6 jsonfield==3.1.0 diff --git a/requirements.txt b/requirements.txt index 2f96d84d61..c729b52c5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -154,7 +154,7 @@ jsonschema-specifications==2024.10.1 # via jsonschema kombu==5.5.2 # via celery -le-utils==0.2.9 +le-utils==0.2.10 # via -r requirements.in packaging==24.2 # via