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/jsonMixin.js b/kolibri/plugins/qti_viewer/assets/src/mixins/jsonMixin.js new file mode 100644 index 00000000000..e0765a92892 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/mixins/jsonMixin.js @@ -0,0 +1,7 @@ +export default { + props: { + json: { + required: true, + }, + }, +}; diff --git a/kolibri/plugins/qti_viewer/assets/src/mixins/qtiMixin.js b/kolibri/plugins/qti_viewer/assets/src/mixins/qtiMixin.js new file mode 100644 index 00000000000..293ea39a584 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/mixins/qtiMixin.js @@ -0,0 +1,3 @@ +export default { + inject: ['getFile', 'getFileString', 'getFilePath', 'getDom'], +}; 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/utils/xml.js b/kolibri/plugins/qti_viewer/assets/src/utils/xml.js new file mode 100644 index 00000000000..517a4e08c34 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/utils/xml.js @@ -0,0 +1,26 @@ +import xmlParser from 'fast-xml-parser'; + +const xmlOptions = { + attributeNamePrefix: '@', + attrNodeName: false, + textNodeName: '#text', + ignoreAttributes: false, + ignoreNameSpace: false, + allowBooleanAttributes: true, + parseNodeValue: true, + parseAttributeValue: true, + trimValues: true, + parseTrueNumberOnly: false, + arrayMode: true, //"strict", +}; + +export default function(xml) { + console.log(xmlParser.parse(xml.trim(), xmlOptions)); + return xmlParser.parse(xml.trim(), xmlOptions); +} + +const jsonToXMLParser = new xmlParser.j2xParser(xmlOptions); + +export function jsonToXML(json) { + return jsonToXMLParser.parse(json); +} 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..7ad62a3d18d --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentItem.vue @@ -0,0 +1,45 @@ + + + + + + + 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..dc472676c2a --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentItemRef.vue @@ -0,0 +1,65 @@ + + + + + + + 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..3453fc101d0 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentSection.vue @@ -0,0 +1,54 @@ + + + + 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..aa878699074 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/AssessmentTest.vue @@ -0,0 +1,55 @@ + + + + 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..4efa274c729 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/ItemBody.vue @@ -0,0 +1,84 @@ + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/ItemLoadingError.vue b/kolibri/plugins/qti_viewer/assets/src/views/ItemLoadingError.vue new file mode 100644 index 00000000000..590ccc376df --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/ItemLoadingError.vue @@ -0,0 +1,21 @@ + + + + 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..4a1d4754a99 --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/QTIViewer.vue @@ -0,0 +1,182 @@ + + + + diff --git a/kolibri/plugins/qti_viewer/assets/src/views/ZipHTML.vue b/kolibri/plugins/qti_viewer/assets/src/views/ZipHTML.vue new file mode 100644 index 00000000000..13e9020012f --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/ZipHTML.vue @@ -0,0 +1,116 @@ + + + + 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..f9c86e7d36e --- /dev/null +++ b/kolibri/plugins/qti_viewer/assets/src/views/interactions/ChoiceInteraction.vue @@ -0,0 +1,92 @@ + + + + + + + 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..60d59343000 --- /dev/null +++ b/kolibri/plugins/qti_viewer/package.json @@ -0,0 +1,11 @@ +{ + "version": "0.1.0", + "name": "qti-viewer", + "description": "A plugin for rendering QTI questions", + "private": true, + "dependencies": { + "fast-xml-parser": "^3.19.0", + "fflate": "^0.7.1", + "xss": "^1.0.9" + } +} diff --git a/requirements/base.txt b/requirements/base.txt index 5bbf82115a4..1950465bbce 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 25d00fdfbe2..aeee042e9b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3703,7 +3703,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== @@ -4157,6 +4157,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" @@ -5456,6 +5461,11 @@ fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-xml-parser@^3.19.0: + version "3.19.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01" + integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg== + fastest-levenshtein@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" @@ -13698,6 +13708,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"