Skip to content

Commit

Permalink
Base QTI rendering implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
rtibbles committed Aug 2, 2021
1 parent 352d07e commit 538b443
Show file tree
Hide file tree
Showing 20 changed files with 898 additions and 2 deletions.
79 changes: 79 additions & 0 deletions kolibri/core/content/migrations/0028_qti_format_preset.py
Original file line number Diff line number Diff line change
@@ -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,
),
),
]
Empty file.
7 changes: 7 additions & 0 deletions kolibri/plugins/qti_viewer/assets/src/mixins/jsonMixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
props: {
json: {
required: true,
},
},
};
3 changes: 3 additions & 0 deletions kolibri/plugins/qti_viewer/assets/src/mixins/qtiMixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
inject: ['getFile', 'getFileString', 'getFilePath', 'getDom'],
};
13 changes: 13 additions & 0 deletions kolibri/plugins/qti_viewer/assets/src/module.js
Original file line number Diff line number Diff line change
@@ -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 };
26 changes: 26 additions & 0 deletions kolibri/plugins/qti_viewer/assets/src/utils/xml.js
Original file line number Diff line number Diff line change
@@ -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);
}
45 changes: 45 additions & 0 deletions kolibri/plugins/qti_viewer/assets/src/views/AssessmentItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>

<div>
{{ title }}

<ItemBody
v-if="itemBody"
:key="identifier"
:json="itemBody"
@submit="$emit('submit', $event)"
/>
</div>

</template>


<script>
import jsonMixin from '../mixins/jsonMixin';
import qtiMixin from '../mixins/qtiMixin';
import ItemBody from './ItemBody';
export default {
name: 'AssessmentItem',
components: {
ItemBody,
},
mixins: [jsonMixin, qtiMixin],
computed: {
itemBody() {
return this.json && this.json.itemBody && this.json.itemBody[0];
},
identifier() {
return this.json && this.json['@identifier'];
},
title() {
return (this.json && this.json['@title']) || '';
},
},
};
</script>


<style lang="scss" scoped></style>
65 changes: 65 additions & 0 deletions kolibri/plugins/qti_viewer/assets/src/views/AssessmentItemRef.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>

<AssessmentItem
v-if="assessmentItem"
:json="assessmentItem"
/>

</template>


<script>
import jsonMixin from '../mixins/jsonMixin';
import qtiMixin from '../mixins/qtiMixin';
import xmlParse from '../utils/xml';
import AssessmentItem from './AssessmentItem';
const domParser = new DOMParser();
export default {
name: 'AssessmentItemRef',
components: {
AssessmentItem,
},
mixins: [jsonMixin, qtiMixin],
provide() {
return {
getDom: this.getAssessmentItemDom,
};
},
data() {
return {
assessmentItem: null,
};
},
computed: {
href() {
const relativeHref = this.json && this.json['@href'];
if (relativeHref) {
return new URL(
relativeHref,
new URL(this.getFilePath(), 'http://b.b/')
).pathname.substring(1);
}
return '';
},
},
created() {
this.getFileString(this.href).then(qtiXML => {
const json = xmlParse(qtiXML);
this.assessmentItemDom = domParser.parseFromString(qtiXML.trim(), 'text/xml');
this.assessmentItem = json.assessmentItem && json.assessmentItem[0];
});
},
methods: {
getAssessmentItemDom() {
return this.assessmentItemDom;
},
},
};
</script>


<style lang="scss" scoped></style>
54 changes: 54 additions & 0 deletions kolibri/plugins/qti_viewer/assets/src/views/AssessmentSection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>

<div>
<AssessmentItem
v-if="assessmentItem"
:json="assessmentItem"
/>
<AssessmentItemRef
v-else-if="assessmentItemRef"
:json="assessmentItemRef"
/>
</div>

</template>


<script>
import jsonMixin from '../mixins/jsonMixin';
import qtiMixin from '../mixins/qtiMixin';
import AssessmentItem from './AssessmentItem';
import AssessmentItemRef from './AssessmentItemRef';
export default {
name: 'AssessmentSection',
components: {
AssessmentItem,
AssessmentItemRef,
},
mixins: [jsonMixin, qtiMixin],
data() {
return {
assessmentItemIndex: 0,
};
},
computed: {
assessmentItem() {
return (
this.json &&
this.json.assessmentItem &&
this.json.assessmentItem[this.assessmentItemIndex]
);
},
assessmentItemRef() {
return (
this.json &&
this.json.assessmentItemRef &&
this.json.assessmentItemRef[this.assessmentItemIndex]
);
},
},
};
</script>
55 changes: 55 additions & 0 deletions kolibri/plugins/qti_viewer/assets/src/views/AssessmentTest.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<template>

<div>
<AssessmentSection
v-if="assessmentSection"
:json="assessmentSection"
/>
<KButton @click="previous">
Back
</KButton>
<KButton @click="next">
Continue
</KButton>
</div>

</template>


<script>
import jsonMixin from '../mixins/jsonMixin';
import qtiMixin from '../mixins/qtiMixin';
import AssessmentSection from './AssessmentSection';
export default {
name: 'AssessmentTest',
components: {
AssessmentSection,
},
mixins: [jsonMixin, qtiMixin],
data() {
return {
testPartIndex: 0,
sectionIndex: 0,
};
},
computed: {
testPart() {
return this.json && this.json.testPart && this.json.testPart[this.testPartIndex];
},
assessmentSection() {
return (
this.testPart &&
this.testPart.assessmentSection &&
this.testPart.assessmentSection[this.sectionIndex]
);
},
},
methods: {
next() {},
previous() {},
},
};
</script>
Loading

0 comments on commit 538b443

Please sign in to comment.