diff --git a/contentcuration/contentcuration/frontend/shared/constants.js b/contentcuration/contentcuration/frontend/shared/constants.js index 0239603c59..29449880ec 100644 --- a/contentcuration/contentcuration/frontend/shared/constants.js +++ b/contentcuration/contentcuration/frontend/shared/constants.js @@ -58,6 +58,7 @@ export const kindToIconMap = { channel: 'apps', document: 'description', exercise: 'star', + h5p: 'widgets', html5: 'widgets', image: 'image', slideshow: 'photo_library', @@ -205,6 +206,7 @@ export const AccessibilityCategoriesMap = { video: ['CAPTIONS_SUBTITLES', 'AUDIO_DESCRIPTION', 'SIGN_LANGUAGE'], exercise: ['ALT_TEXT'], html5: ['ALT_TEXT', 'HIGH_CONTRAST'], + h5p: ['ALT_TEXT', 'HIGH_CONTRAST'], audio: ['CAPTIONS_SUBTITLES'], }; diff --git a/contentcuration/contentcuration/frontend/shared/leUtils/FormatPresets.js b/contentcuration/contentcuration/frontend/shared/leUtils/FormatPresets.js index b7e165b0e2..4dc73e8fa4 100644 --- a/contentcuration/contentcuration/frontend/shared/leUtils/FormatPresets.js +++ b/contentcuration/contentcuration/frontend/shared/leUtils/FormatPresets.js @@ -185,7 +185,7 @@ const FormatPresetsMap = new Map([ supplementary: false, thumbnail: false, subtitle: false, - display: false, + display: true, order: 1, kind_id: 'h5p', allowed_formats: ['h5p'], diff --git a/contentcuration/contentcuration/frontend/shared/leUtils/Languages.js b/contentcuration/contentcuration/frontend/shared/leUtils/Languages.js index b45b4f9594..a3b5373ea2 100644 --- a/contentcuration/contentcuration/frontend/shared/leUtils/Languages.js +++ b/contentcuration/contentcuration/frontend/shared/leUtils/Languages.js @@ -66,6 +66,17 @@ const LanguagesMap = new Map([ lang_direction: 'ltr', }, ], + [ + 'rsn', + { + id: 'rsn', + lang_code: 'rsn', + lang_subcode: null, + readable_name: 'Rwandan Sign Language', + native_name: "Amarenga y'Ikinyarwanda", + lang_direction: 'ltr', + }, + ], [ 'an', { @@ -198,6 +209,17 @@ const LanguagesMap = new Map([ lang_direction: 'ltr', }, ], + [ + 'csx', + { + id: 'csx', + lang_code: 'csx', + lang_subcode: null, + readable_name: 'Cambodian Sign Language', + native_name: 'Cambodian Sign Language', + lang_direction: 'ltr', + }, + ], [ 'ca', { @@ -1430,6 +1452,17 @@ const LanguagesMap = new Map([ lang_direction: 'ltr', }, ], + [ + 'xki', + { + id: 'xki', + lang_code: 'xki', + lang_subcode: null, + readable_name: 'Kenyan Sign Language', + native_name: 'Swahili Lugha ya ishara', + lang_direction: 'ltr', + }, + ], [ 'be-tara', { @@ -2564,6 +2597,18 @@ const LanguagesMap = new Map([ lang_direction: 'ltr', }, ], + [ + 'nsp', + { + id: 'nsp', + lang_code: 'nsp', + lang_subcode: null, + readable_name: 'Nepalese Sign Language', + native_name: + '\u0928\u0947\u092a\u093e\u0932\u0940 \u0938\u093e\u0902\u0915\u0947\u0924\u093f\u0915 \u092d\u093e\u0937\u093e', + lang_direction: 'ltr', + }, + ], [ 'pi', { @@ -3011,6 +3056,7 @@ export const LanguagesNames = { AF: 'af', AK: 'ak', AKA: 'aka', + RSN: 'rsn', AN: 'an', EN_PT: 'en-PT', IG: 'ig', @@ -3023,6 +3069,7 @@ export const LanguagesNames = { EN_GB: 'en-GB', BUG: 'bug', BXK: 'bxk', + CSX: 'csx', CA: 'ca', CEB: 'ceb', CH: 'ch', @@ -3135,6 +3182,7 @@ export const LanguagesNames = { PBT: 'pbt', SV_SE: 'sv-SE', SV_FI: 'sv-FI', + XKI: 'xki', BE_TARA: 'be-tara', VI: 'vi', TPI: 'tpi', @@ -3237,6 +3285,7 @@ export const LanguagesNames = { NEW: 'new', NE: 'ne', NE_NP: 'ne-NP', + NSP: 'nsp', PI: 'pi', BH: 'bh', BHO: 'bho', diff --git a/contentcuration/contentcuration/frontend/shared/views/ContentNodeIcon.vue b/contentcuration/contentcuration/frontend/shared/views/ContentNodeIcon.vue index 874b084810..770c3225cf 100644 --- a/contentcuration/contentcuration/frontend/shared/views/ContentNodeIcon.vue +++ b/contentcuration/contentcuration/frontend/shared/views/ContentNodeIcon.vue @@ -84,6 +84,8 @@ return this.$tr('exercise'); case 'document': return this.$tr('document'); + case 'h5p': + return this.$tr('html5'); case 'html5': return this.$tr('html5'); default: diff --git a/contentcuration/contentcuration/frontend/shared/vuetify/theme.js b/contentcuration/contentcuration/frontend/shared/vuetify/theme.js index a23e99dd25..add22c47ef 100644 --- a/contentcuration/contentcuration/frontend/shared/vuetify/theme.js +++ b/contentcuration/contentcuration/frontend/shared/vuetify/theme.js @@ -20,6 +20,7 @@ export default function theme() { audio: '#f06292', document: '#ff3d00', exercise: '#4db6ac', + h5p: '#ff8f00', html5: '#ff8f00', zim: '#ff8f00', slideshow: '#4ece90', diff --git a/contentcuration/contentcuration/frontend/shared/vuex/file/__tests__/module.spec.js b/contentcuration/contentcuration/frontend/shared/vuex/file/__tests__/module.spec.js index cddc6cfc53..3f38ceabd8 100644 --- a/contentcuration/contentcuration/frontend/shared/vuex/file/__tests__/module.spec.js +++ b/contentcuration/contentcuration/frontend/shared/vuex/file/__tests__/module.spec.js @@ -149,17 +149,17 @@ describe('file store', () => { it('getH5PMetadata should check for h5p.json file', () => { const zip = new JSZip(); return zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) { - await expect(Promise.resolve(getH5PMetadata(h5pBlob))).resolves.toThrowError( + await expect(getH5PMetadata(h5pBlob)).rejects.toThrow( 'h5p.json not found in the H5P file.' ); }); }); - it('getH5PMetadata should exract metadata from h5p.json', async () => { + it('getH5PMetadata should extract metadata from h5p.json', async () => { const manifestFile = get_metadata_file({ title: 'Test file' }); const zip = new JSZip(); zip.file('h5p.json', manifestFile); await zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) { - await expect(Promise.resolve(getH5PMetadata(h5pBlob))).resolves.toEqual({ + await expect(getH5PMetadata(h5pBlob)).resolves.toEqual({ title: 'Test file', }); }); @@ -169,7 +169,55 @@ describe('file store', () => { const zip = new JSZip(); zip.file('h5p.json', manifestFile); await zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) { - await expect(Promise.resolve(getH5PMetadata(h5pBlob))).resolves.toEqual({ + await expect(getH5PMetadata(h5pBlob)).resolves.toEqual({ + title: 'Test file', + }); + }); + }); + it.each([ + ['CC BY', 1], + ['CC BY-SA', 2], + ['CC BY-ND', 3], + ['CC BY-NC', 4], + ['CC BY-NC-SA', 5], + ['CC BY-NC-ND', 6], + ['CC0 1.0', 8], + ])('getH5PMetadata should parse CC license %s', async (licenseName, licenseId) => { + const manifestFile = get_metadata_file({ title: 'Test file', license: licenseName }); + const zip = new JSZip(); + zip.file('h5p.json', manifestFile); + await zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) { + await expect(getH5PMetadata(h5pBlob)).resolves.toEqual({ + title: 'Test file', + license: licenseId, + }); + }); + }); + it.each([ + [{ role: 'Author', name: 'Testing' }, 'author'], + [{ role: 'Editor', name: 'Testing' }, 'aggregator'], + [{ role: 'Licensee', name: 'Testing' }, 'copyright_holder'], + [{ role: 'Originator', name: 'Testing' }, 'provider'], + ])('getH5PMetadata should parse CC license %s', async (authorObj, field) => { + const manifestFile = get_metadata_file({ title: 'Test file', authors: [authorObj] }); + const zip = new JSZip(); + zip.file('h5p.json', manifestFile); + await zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) { + await expect(getH5PMetadata(h5pBlob)).resolves.toEqual({ + title: 'Test file', + [field]: authorObj.name, + }); + }); + }); + it('getH5PMetadata should not extract Firstname Surname author', async () => { + const manifestFile = get_metadata_file({ + title: 'Test file', + authors: [{ name: 'Firstname Surname', role: 'Author' }], + }); + const zip = new JSZip(); + zip.file('h5p.json', manifestFile); + await zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) { + await expect(getH5PMetadata(h5pBlob)).resolves.toEqual({ title: 'Test file', }); }); @@ -178,17 +226,13 @@ describe('file store', () => { const manifestFile = get_metadata_file({ title: 'Test file', language: 'en', - authors: 'author1', - license: 'license1', }); const zip = new JSZip(); zip.file('h5p.json', manifestFile); await zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) { - await expect(Promise.resolve(getH5PMetadata(h5pBlob))).resolves.toEqual({ + await expect(getH5PMetadata(h5pBlob)).resolves.toEqual({ title: 'Test file', language: 'en', - author: 'author1', - license: 'license1', }); }); }); diff --git a/contentcuration/contentcuration/frontend/shared/vuex/file/utils.js b/contentcuration/contentcuration/frontend/shared/vuex/file/utils.js index ed67a34bc0..12e59b943c 100644 --- a/contentcuration/contentcuration/frontend/shared/vuex/file/utils.js +++ b/contentcuration/contentcuration/frontend/shared/vuex/file/utils.js @@ -1,6 +1,8 @@ import SparkMD5 from 'spark-md5'; import JSZip from 'jszip'; import { FormatPresetsList, FormatPresetsNames } from 'shared/leUtils/FormatPresets'; +import { LicensesList } from 'shared/leUtils/Licenses'; +import LanguagesMap from 'shared/leUtils/Languages'; const BLOB_SLICE = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; const CHUNK_SIZE = 2097152; @@ -64,6 +66,13 @@ export function storageUrl(checksum, file_format) { return `/content/storage/${checksum[0]}/${checksum[1]}/${checksum}.${file_format}`; } +const AuthorFieldMappings = { + Author: 'author', + Editor: 'aggregator', + Licensee: 'copyright_holder', + Originator: 'provider', +}; + export async function getH5PMetadata(fileInput) { const zip = new JSZip(); const metadata = {}; @@ -82,19 +91,34 @@ export async function getH5PMetadata(fileInput) { if (Object.prototype.hasOwnProperty.call(data, 'title')) { metadata.title = data['title']; } - if (Object.prototype.hasOwnProperty.call(data, 'language') && data['language'] !== 'und') { + if ( + Object.prototype.hasOwnProperty.call(data, 'language') && + LanguagesMap.has(data['language']) && + data['language'] !== 'und' + ) { metadata.language = data['language']; } if (Object.prototype.hasOwnProperty.call(data, 'authors')) { - metadata.author = data['authors']; + for (const author of data['authors']) { + // Ignore obvious placedholders created by online H5P editor tools + if (author.role && author.name !== 'Firstname Surname') { + if (AuthorFieldMappings[author.role]) { + metadata[AuthorFieldMappings[author.role]] = author.name; + } + } + } } if (Object.prototype.hasOwnProperty.call(data, 'license')) { - metadata.license = data['license']; + const license = LicensesList.find(license => license.license_name === data['license']); + if (license) { + metadata.license = license.id; + } else if (data['license'] == 'CC0 1.0') { + // Special case for CC0 1.0 + // this is the hard coded license id for CC0 1.0 + metadata.license = 8; + } } return metadata; - }) - .catch(function(error) { - return error; }); } @@ -130,7 +154,7 @@ export function extractMetadata(file, preset = null) { return new Promise(resolve => { if (isH5P) { getH5PMetadata(file).then(data => { - if (data.constructor !== Error) Object.assign(metadata, ...data); + Object.assign(metadata, data); }); resolve(metadata); } else {