From d549179edf23cf20a876be48a4003e21c680c98a Mon Sep 17 00:00:00 2001 From: Samson Akol Date: Mon, 10 Mar 2025 18:39:19 +0300 Subject: [PATCH 1/8] Removes redundant check due to vue's reactivity --- .../channelEdit/components/AnswersEditor/AnswersEditor.vue | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue b/contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue index aa66b9d5fd..ce88ccec35 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue @@ -388,10 +388,6 @@ } }, updateAnswerText(newAnswerText, answerIdx) { - if (newAnswerText === this.answers[answerIdx].answer) { - return; - } - const updatedAnswers = [...this.answers]; updatedAnswers[answerIdx].answer = newAnswerText; From 69edcd6df2c75d77ea579e45bd22521f72bd3ce0 Mon Sep 17 00:00:00 2001 From: Samson Akol Date: Mon, 10 Mar 2025 18:36:36 +0300 Subject: [PATCH 2/8] Adds update event to number field text --- .../channelEdit/components/AnswersEditor/AnswersEditor.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue b/contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue index ce88ccec35..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)" /> From a8e8856cffc9797e178557b7bc8656fae324fe29 Mon Sep 17 00:00:00 2001 From: ozer550 Date: Tue, 8 Apr 2025 12:31:26 +0530 Subject: [PATCH 3/8] delay the update call so that vuex store is properly populated --- .../vuex/assessmentItem/actions.js | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js index 8452cc0641..d0f9de366c 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js @@ -3,18 +3,31 @@ 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 } - ); +function updateNodeComplete(nodeId, context, maxTries = 10, delayMs = 100) { + let tries = 0; + + function tryUpdate() { + const node = context.rootGetters['contentNode/getContentNode'](nodeId); + if (node) { + console.log('number of tries: ', tries); + 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(); } /** From 2d027682efaac0b4a6f87261ed1f6fda00f43ce7 Mon Sep 17 00:00:00 2001 From: ozer550 Date: Tue, 8 Apr 2025 12:34:17 +0530 Subject: [PATCH 4/8] remove console log --- .../frontend/channelEdit/vuex/assessmentItem/actions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js index d0f9de366c..27b5a6c10d 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js @@ -9,7 +9,6 @@ function updateNodeComplete(nodeId, context, maxTries = 10, delayMs = 100) { function tryUpdate() { const node = context.rootGetters['contentNode/getContentNode'](nodeId); if (node) { - console.log('number of tries: ', tries); const complete = isNodeComplete({ nodeDetails: node, assessmentItems: context.getters.getAssessmentItems(nodeId), From 8828359d5814723c242ba0d22b3734d91b7607c3 Mon Sep 17 00:00:00 2001 From: Jacob Pierce Date: Fri, 4 Apr 2025 13:54:04 -0700 Subject: [PATCH 5/8] raise rest_framework.ValidationError when lft is not an integer in ContentNodePagination --- contentcuration/contentcuration/viewsets/contentnode.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contentcuration/contentcuration/viewsets/contentnode.py b/contentcuration/contentcuration/viewsets/contentnode.py index 612e38807c..a65a7ff150 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 as e: + raise ValidationError("lft must be an integer but {} was given: {}".format(value,e)) + return Cursor(offset=0, reverse=False, position=value) def encode_cursor(self, cursor): From cd3e5a83fa172559e714d7742621bdb98e49a1be Mon Sep 17 00:00:00 2001 From: ozer550 Date: Wed, 16 Apr 2025 12:54:58 +0530 Subject: [PATCH 6/8] add comments reflecting the reasoning behind the workaround --- .../frontend/channelEdit/vuex/assessmentItem/actions.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js index 27b5a6c10d..802e46c233 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/actions.js @@ -3,6 +3,9 @@ import { isNodeComplete } from 'shared/utils/validation'; import db from 'shared/data/db'; import { TABLE_NAMES } from 'shared/data/constants'; +// 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; From 0a2a718197bcf3cbcfcb14cfc0f6c76a4aeeabff Mon Sep 17 00:00:00 2001 From: Jacob Pierce Date: Wed, 16 Apr 2025 14:35:27 -0700 Subject: [PATCH 7/8] remove user input from pagination error --- contentcuration/contentcuration/viewsets/contentnode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contentcuration/contentcuration/viewsets/contentnode.py b/contentcuration/contentcuration/viewsets/contentnode.py index a65a7ff150..76acb56667 100644 --- a/contentcuration/contentcuration/viewsets/contentnode.py +++ b/contentcuration/contentcuration/viewsets/contentnode.py @@ -691,8 +691,8 @@ def decode_cursor(self, request): try: value = int(value) - except ValueError as e: - raise ValidationError("lft must be an integer but {} was given: {}".format(value,e)) + except ValueError: + raise ValidationError("lft must be an integer but an invalid value was given.") return Cursor(offset=0, reverse=False, position=value) From 9a1832d2a331103a805d468853c86e5fd5987ec3 Mon Sep 17 00:00:00 2001 From: Blaine Jester Date: Thu, 17 Apr 2025 08:19:23 -0700 Subject: [PATCH 8/8] Upgrade le-utils to 0.2.10 --- .../migrations/0151_auto_20250417_1516.py | 87 +++++++++++++ .../migrations/0023_auto_20250417_1516.py | 115 ++++++++++++++++++ .../migrations/0006_auto_20250417_1516.py | 85 +++++++++++++ requirements.in | 2 +- requirements.txt | 2 +- 5 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 contentcuration/contentcuration/migrations/0151_auto_20250417_1516.py create mode 100644 contentcuration/kolibri_content/migrations/0023_auto_20250417_1516.py create mode 100644 contentcuration/kolibri_public/migrations/0006_auto_20250417_1516.py 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/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