diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue b/contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue index aa66b9d5fd..cedc875b88 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue @@ -63,6 +63,7 @@ class="answer-number" type="number" :rules="[numericRule]" + @change="updateAnswerText($event, answerIdx)" /> @@ -388,10 +389,6 @@ } }, updateAnswerText(newAnswerText, answerIdx) { - if (newAnswerText === this.answers[answerIdx].answer) { - return; - } - const updatedAnswers = [...this.answers]; updatedAnswers[answerIdx].answer = newAnswerText; 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_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/viewsets/contentnode.py b/contentcuration/contentcuration/viewsets/contentnode.py index 612e38807c..76acb56667 100644 --- a/contentcuration/contentcuration/viewsets/contentnode.py +++ b/contentcuration/contentcuration/viewsets/contentnode.py @@ -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 d9769e45aa..75d36b7c0c 100644 --- a/requirements.in +++ b/requirements.in @@ -5,7 +5,7 @@ djangorestframework==3.15.1 psycopg2-binary==2.9.5 django-js-reverse==0.9.1 django-registration==3.4 -le-utils==0.2.5 +le-utils==0.2.10 gunicorn==20.1.0 django-postmark==0.1.6 jsonfield==3.1.0 diff --git a/requirements.txt b/requirements.txt index 525d9d2e57..aeb89060ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -155,7 +155,7 @@ jsonschema==4.17.3 # via -r requirements.in kombu==5.2.4 # via celery -le-utils==0.2.7 +le-utils==0.2.10 # via -r requirements.in packaging==24.0 # via