From 8153bf94fca0573c364116bd96214f438cadf4c8 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Mon, 10 May 2021 14:17:01 -0700 Subject: [PATCH] Basic QTI zip loading prototype. --- kolibri/core/assets/src/core-app/urls.js | 4 + .../migrations/0028_qti_format_preset.py | 79 +++++++++++ kolibri/plugins/qti_viewer/__init__.py | 0 .../qti_viewer/assets/src/mixins/domMixin.js | 19 +++ .../plugins/qti_viewer/assets/src/module.js | 13 ++ .../assets/src/views/AssessmentItem.vue | 46 +++++++ .../assets/src/views/AssessmentItemRef.vue | 58 ++++++++ .../assets/src/views/AssessmentSection.vue | 55 ++++++++ .../assets/src/views/AssessmentTest.vue | 58 ++++++++ .../qti_viewer/assets/src/views/HTML.vue | 105 +++++++++++++++ .../qti_viewer/assets/src/views/ItemBody.vue | 41 ++++++ .../qti_viewer/assets/src/views/QTIViewer.vue | 124 ++++++++++++++++++ .../views/interactions/ChoiceInteraction.vue | 88 +++++++++++++ .../src/views/interactions/ChoiceMultiple.vue | 62 +++++++++ .../assets/src/views/interactions/Prompt.vue | 28 ++++ kolibri/plugins/qti_viewer/buildConfig.js | 6 + kolibri/plugins/qti_viewer/kolibri_plugin.py | 19 +++ kolibri/plugins/qti_viewer/package.json | 9 ++ requirements/base.txt | 2 +- yarn.lock | 15 ++- 20 files changed, 829 insertions(+), 2 deletions(-) create mode 100644 kolibri/core/content/migrations/0028_qti_format_preset.py create mode 100644 kolibri/plugins/qti_viewer/__init__.py create mode 100644 kolibri/plugins/qti_viewer/assets/src/mixins/domMixin.js create mode 100644 kolibri/plugins/qti_viewer/assets/src/module.js create mode 100644 kolibri/plugins/qti_viewer/assets/src/views/AssessmentItem.vue create mode 100644 kolibri/plugins/qti_viewer/assets/src/views/AssessmentItemRef.vue create mode 100644 kolibri/plugins/qti_viewer/assets/src/views/AssessmentSection.vue create mode 100644 kolibri/plugins/qti_viewer/assets/src/views/AssessmentTest.vue create mode 100644 kolibri/plugins/qti_viewer/assets/src/views/HTML.vue create mode 100644 kolibri/plugins/qti_viewer/assets/src/views/ItemBody.vue create mode 100644 kolibri/plugins/qti_viewer/assets/src/views/QTIViewer.vue create mode 100644 kolibri/plugins/qti_viewer/assets/src/views/interactions/ChoiceInteraction.vue create mode 100644 kolibri/plugins/qti_viewer/assets/src/views/interactions/ChoiceMultiple.vue create mode 100644 kolibri/plugins/qti_viewer/assets/src/views/interactions/Prompt.vue create mode 100644 kolibri/plugins/qti_viewer/buildConfig.js create mode 100644 kolibri/plugins/qti_viewer/kolibri_plugin.py create mode 100644 kolibri/plugins/qti_viewer/package.json diff --git a/kolibri/core/assets/src/core-app/urls.js b/kolibri/core/assets/src/core-app/urls.js index 8c054779acf..cb155775a9f 100644 --- a/kolibri/core/assets/src/core-app/urls.js +++ b/kolibri/core/assets/src/core-app/urls.js @@ -48,6 +48,10 @@ const urls = { } return generateUrl(this.__mediaUrl, { url }); }, + rawStorageUrl(fileId, extension) { + const filename = `${fileId}.${extension}`; + return generateUrl(this.__contentUrl, { url: `${filename[0]}/${filename[1]}/${filename}` }); + }, storageUrl(fileId, extension, embeddedFilePath = '') { const filename = `${fileId}.${extension}`; if (['zip', 'h5p'].includes(extension)) { diff --git a/kolibri/core/content/migrations/0028_qti_format_preset.py b/kolibri/core/content/migrations/0028_qti_format_preset.py new file mode 100644 index 00000000000..4bfc0da8b5d --- /dev/null +++ b/kolibri/core/content/migrations/0028_qti_format_preset.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2021-05-10 21:49 +from __future__ import unicode_literals + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0027_channelmetadata_tagline"), + ] + + 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"), + ("qti", "QTI Zip"), + ("qti_thumbnail", "QTI Thumbnail"), + ("slideshow_image", "Slideshow Image"), + ("slideshow_thumbnail", "Slideshow Thumbnail"), + ("slideshow_manifest", "Slideshow Manifest"), + ], + 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"), + ("epub", "ePub Document"), + ], + max_length=40, + ), + ), + ] diff --git a/kolibri/plugins/qti_viewer/__init__.py b/kolibri/plugins/qti_viewer/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/kolibri/plugins/qti_viewer/assets/src/mixins/domMixin.js b/kolibri/plugins/qti_viewer/assets/src/mixins/domMixin.js new file mode 100644 index 00000000000..9a6d8bd4271 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/mixins/domMixin.js @@ -0,0 +1,19 @@ +export default { + props: { + dom: { + required: true, + }, + qtiFile: { + required: true, + }, + filePath: { + required: true, + type: String, + }, + }, + computed: { + children() { + return Array.from(this.dom.children); + }, + }, +}; diff --git a/kolibri/plugins/qti_viewer/assets/src/module.js b/kolibri/plugins/qti_viewer/assets/src/module.js new file mode 100644 index 00000000000..a08157c166b --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/module.js @@ -0,0 +1,13 @@ +import QTIViewer from './views/QTIViewer'; +import ContentRendererModule from 'content_renderer_module'; + +class QTIViewerModule extends ContentRendererModule { + get rendererComponent() { + QTIViewer.contentModule = this; + return QTIViewer; + } +} + +const qtiViewer = new QTIViewerModule(); + +export { qtiViewer as default }; diff --git a/kolibri/plugins/qti_viewer/assets/src/views/AssessmentItem.vue b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentItem.vue new file mode 100644 index 00000000000..4d6fe364ebf --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentItem.vue @@ -0,0 +1,46 @@ + + + + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/AssessmentItemRef.vue b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentItemRef.vue new file mode 100644 index 00000000000..f0a9bbf58bf --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentItemRef.vue @@ -0,0 +1,58 @@ + + + + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/AssessmentSection.vue b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentSection.vue new file mode 100644 index 00000000000..2b6c2666e13 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentSection.vue @@ -0,0 +1,55 @@ + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/AssessmentTest.vue b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentTest.vue new file mode 100644 index 00000000000..5cee132c0d8 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentTest.vue @@ -0,0 +1,58 @@ + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/HTML.vue b/kolibri/plugins/qti_viewer/assets/src/views/HTML.vue new file mode 100644 index 00000000000..eb6bfbd1e09 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/HTML.vue @@ -0,0 +1,105 @@ + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/ItemBody.vue b/kolibri/plugins/qti_viewer/assets/src/views/ItemBody.vue new file mode 100644 index 00000000000..d9ef2cf3599 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/ItemBody.vue @@ -0,0 +1,41 @@ + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/QTIViewer.vue b/kolibri/plugins/qti_viewer/assets/src/views/QTIViewer.vue new file mode 100644 index 00000000000..c08e5a1998e --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/QTIViewer.vue @@ -0,0 +1,124 @@ + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/interactions/ChoiceInteraction.vue b/kolibri/plugins/qti_viewer/assets/src/views/interactions/ChoiceInteraction.vue new file mode 100644 index 00000000000..3a6f0c0b7e8 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/interactions/ChoiceInteraction.vue @@ -0,0 +1,88 @@ + + + + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/interactions/ChoiceMultiple.vue b/kolibri/plugins/qti_viewer/assets/src/views/interactions/ChoiceMultiple.vue new file mode 100644 index 00000000000..cb71cc53cb8 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/interactions/ChoiceMultiple.vue @@ -0,0 +1,62 @@ + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/interactions/Prompt.vue b/kolibri/plugins/qti_viewer/assets/src/views/interactions/Prompt.vue new file mode 100644 index 00000000000..e746f5f4648 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/interactions/Prompt.vue @@ -0,0 +1,28 @@ + + + + + + + diff --git a/kolibri/plugins/qti_viewer/buildConfig.js b/kolibri/plugins/qti_viewer/buildConfig.js new file mode 100644 index 00000000000..860f9b46554 --- /dev/null +++ b/kolibri/plugins/qti_viewer/buildConfig.js @@ -0,0 +1,6 @@ +module.exports = { + bundle_id: 'main', + webpack_config: { + entry: './assets/src/module.js', + }, +}; diff --git a/kolibri/plugins/qti_viewer/kolibri_plugin.py b/kolibri/plugins/qti_viewer/kolibri_plugin.py new file mode 100644 index 00000000000..6f9f9df2983 --- /dev/null +++ b/kolibri/plugins/qti_viewer/kolibri_plugin.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +from le_utils.constants import format_presets + +from kolibri.core.content import hooks as content_hooks +from kolibri.plugins import KolibriPluginBase +from kolibri.plugins.hooks import register_hook + + +class QTIViewerPlugin(KolibriPluginBase): + pass + + +@register_hook +class QTIViewerAsset(content_hooks.ContentRendererHook): + bundle_id = "main" + presets = (format_presets.QTI_ZIP,) diff --git a/kolibri/plugins/qti_viewer/package.json b/kolibri/plugins/qti_viewer/package.json new file mode 100644 index 00000000000..5159a2c2fd7 --- /dev/null +++ b/kolibri/plugins/qti_viewer/package.json @@ -0,0 +1,9 @@ +{ + "version": "0.1.0", + "name": "qti-viewer", + "description": "A plugin for rendering QTI questions", + "private": true, + "dependencies": { + "xss": "^1.0.9" + } +} diff --git a/requirements/base.txt b/requirements/base.txt index 00695874f57..4d22b89a6ab 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -12,7 +12,7 @@ cheroot==8.5.2 magicbus==4.1.2 futures==3.1.1 # Temporarily pinning this until we can do a Python 2/3 compatible solution of newer versions # pyup: <=3.1.1 more-itertools==5.0.0 # Last Python 2.7 friendly release # pyup: <6.0 -le-utils==0.1.24 +le-utils==0.1.30 kolibri_exercise_perseus_plugin==1.3.5 jsonfield==2.0.2 requests-toolbelt==0.8.0 diff --git a/yarn.lock b/yarn.lock index 8e4dae4d6c7..4e9e1393f66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4114,7 +4114,7 @@ commander@2.6.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" integrity sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0= -commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.9.0: +commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.9.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -4546,6 +4546,11 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssfilter@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae" + integrity sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4= + csslint@0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/csslint/-/csslint-0.10.0.tgz#3a6a04e7565c8e9d19beb49767c7ec96e8365805" @@ -15238,6 +15243,14 @@ xmldom@^0.1.27: resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== +xss@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.9.tgz#3ffd565571ff60d2e40db7f3b80b4677bec770d2" + integrity sha512-2t7FahYnGJys6DpHLhajusId7R0Pm2yTmuL0GV9+mV0ZlaLSnb2toBmppATfg5sWIhZQGlsTLoecSzya+l4EAQ== + dependencies: + commander "^2.20.3" + cssfilter "0.0.10" + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"