From 6281b7536dbbea449ecb70719b93e515c0637aa4 Mon Sep 17 00:00:00 2001 From: Christian Einvik <84850107+chrieinv@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:07:02 +0200 Subject: [PATCH] Update h5p packages (#2291) * Update h5p/h5p-core and h5p/h5p-editor Composer packages, use patch version in library folder name --- sourcecode/apis/contentauthor/.gitignore | 2 + .../app/Console/Commands/PublishPresave.php | 2 +- .../apis/contentauthor/app/H5PContent.php | 9 +- .../app/H5PLibrariesHubCache.php | 16 +- .../apis/contentauthor/app/H5PLibrary.php | 77 +- .../Admin/AdminH5PDetailsController.php | 2 +- .../app/Http/Controllers/H5PController.php | 2 +- .../app/Libraries/H5P/AjaxRequest.php | 14 +- .../app/Libraries/H5P/EditorAjax.php | 2 +- .../app/Libraries/H5P/EditorStorage.php | 4 +- .../app/Libraries/H5P/Framework.php | 60 +- .../app/Libraries/H5P/H5PExport.php | 4 +- .../app/Libraries/H5P/H5PViewConfig.php | 2 +- .../H5P/Interfaces/CerpusStorageInterface.php | 4 - .../H5P/Storage/H5PCerpusStorage.php | 23 +- sourcecode/apis/contentauthor/composer.json | 4 +- sourcecode/apis/contentauthor/composer.lock | 40 +- ..._in_folder_name_to_h5p_libraries_table.php | 31 + .../js/h5p/core-override/h5p-content-type.js | 41 + .../h5p/{ => core-override}/request-queue.js | 0 .../assets/entrypoints/h5p-core-bundle.js | 5 +- .../assets/entrypoints/h5p-core.scss | 1 + .../resources/views/h5p/show.blade.php | 4 +- .../Integration/Article/ArticleLockTest.php | 1 + .../tests/Integration/Article/ArticleTest.php | 8 +- .../Article/ArticleVersioningTest.php | 1 + .../tests/Integration/H5PContentTest.php | 64 ++ .../Integration/H5PLibrariesHubCacheTest.php | 32 + .../Http/Controllers/H5PControllerTest.php | 29 +- .../H5P/API/H5PImportControllerTest.php | 6 +- .../Libraries/H5P/AjaxRequestTest.php | 64 ++ .../Integration/Libraries/H5P/CRUTest.php | 5 +- .../Libraries/H5P/EditorAjaxTest.php | 74 ++ .../Libraries/H5P/EditorStorageTest.php | 58 + .../Libraries/H5P/FrameworkTest.php | 206 ++++ .../Libraries/H5P/H5PExportTest.php | 35 +- .../H5P/Storage/H5pCerpusStorageTest.php | 64 +- .../Integration/Models/H5PLibraryTest.php | 100 ++ .../AdminH5PDetailsControllerTest.php | 11 +- .../Unit/Libraries/H5P/FrameworkTest.php | 26 + .../H5P.Blanks-1.14.6/css/blanks.css | 113 ++ .../libraries/H5P.Blanks-1.14.6/icon.svg | 68 ++ .../libraries/H5P.Blanks-1.14.6/js/blanks.js | 1004 +++++++++++++++++ .../libraries/H5P.Blanks-1.14.6/js/cloze.js | 238 ++++ .../H5P.Blanks-1.14.6/language/nb.json | 214 ++++ .../libraries/H5P.Blanks-1.14.6/library.json | 65 ++ .../libraries/H5P.Blanks-1.14.6/presave.js | 40 + .../H5P.Blanks-1.14.6/semantics.json | 489 ++++++++ .../libraries/H5P.Blanks-1.14.6/upgrades.js | 124 ++ 49 files changed, 3372 insertions(+), 116 deletions(-) create mode 100644 sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php create mode 100755 sourcecode/apis/contentauthor/public/js/h5p/core-override/h5p-content-type.js rename sourcecode/apis/contentauthor/public/js/h5p/{ => core-override}/request-queue.js (100%) mode change 100644 => 100755 create mode 100644 sourcecode/apis/contentauthor/tests/Integration/H5PContentTest.php create mode 100644 sourcecode/apis/contentauthor/tests/Integration/H5PLibrariesHubCacheTest.php create mode 100644 sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php create mode 100644 sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php create mode 100644 sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php create mode 100644 sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php create mode 100644 sourcecode/apis/contentauthor/tests/Integration/Models/H5PLibraryTest.php create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/css/blanks.css create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/icon.svg create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/blanks.js create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/cloze.js create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/language/nb.json create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/library.json create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/presave.js create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/semantics.json create mode 100644 sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/upgrades.js diff --git a/sourcecode/apis/contentauthor/.gitignore b/sourcecode/apis/contentauthor/.gitignore index a6ae563e73..b9730013e9 100644 --- a/sourcecode/apis/contentauthor/.gitignore +++ b/sourcecode/apis/contentauthor/.gitignore @@ -31,10 +31,12 @@ Homestead.json !/public/js/videos/brightcove.js !/public/js/videos/streamps.js !/public/js/h5p/ndlah5p-youtube.js +!public/js/h5p/core-override !/public/js/h5p/h5peditor-pre-save.js !public/css /public/css/* !/public/css/ndlah5p-youtube.css + mix-manifest.json app/Libraries/oauth-php/example/server/cache/ ca1.sql diff --git a/sourcecode/apis/contentauthor/app/Console/Commands/PublishPresave.php b/sourcecode/apis/contentauthor/app/Console/Commands/PublishPresave.php index d54850953c..bc2383837e 100644 --- a/sourcecode/apis/contentauthor/app/Console/Commands/PublishPresave.php +++ b/sourcecode/apis/contentauthor/app/Console/Commands/PublishPresave.php @@ -57,7 +57,7 @@ public function handle(): void private static function getDestination(H5PLibrary $library): string { - $directory = $library->getLibraryString(true); + $directory = $library->getFolderName(); return "libraries/$directory/presave.js"; } diff --git a/sourcecode/apis/contentauthor/app/H5PContent.php b/sourcecode/apis/contentauthor/app/H5PContent.php index a9e4838e64..bb89121d20 100644 --- a/sourcecode/apis/contentauthor/app/H5PContent.php +++ b/sourcecode/apis/contentauthor/app/H5PContent.php @@ -344,14 +344,7 @@ public static function getContentTypeInfo(string $contentType): ?ContentTypeData if ($library->has_icon) { $h5pFramework = app(H5PFrameworkInterface::class); - - $library_folder = H5PCore::libraryToString([ - 'machineName' => $library->machine_name, - 'majorVersion' => $library->major_version, - 'minorVersion' => $library->minor_version - ], true); - - + $library_folder = $library->getFolderName(); $icon_path = $h5pFramework->getLibraryFileUrl($library_folder, 'icon.svg'); if (!empty($icon_path)) { diff --git a/sourcecode/apis/contentauthor/app/H5PLibrariesHubCache.php b/sourcecode/apis/contentauthor/app/H5PLibrariesHubCache.php index 895e297860..722ed01c64 100644 --- a/sourcecode/apis/contentauthor/app/H5PLibrariesHubCache.php +++ b/sourcecode/apis/contentauthor/app/H5PLibrariesHubCache.php @@ -45,10 +45,16 @@ public function libraries(): HasMany public function getLibraryString($folderName = false) { - return \H5PCore::libraryToString([ - 'machineName' => $this->name, - 'majorVersion' => $this->major_version, - 'minorVersion' => $this->minor_version, - ], $folderName); + return $folderName ? + \H5PCore::libraryToFolderName([ + 'machineName' => $this->name, + 'majorVersion' => $this->major_version, + 'minorVersion' => $this->minor_version, + ]) : + \H5PCore::libraryToString([ + 'machineName' => $this->name, + 'majorVersion' => $this->major_version, + 'minorVersion' => $this->minor_version, + ]); } } diff --git a/sourcecode/apis/contentauthor/app/H5PLibrary.php b/sourcecode/apis/contentauthor/app/H5PLibrary.php index b0577e85e7..6316f5ecb2 100644 --- a/sourcecode/apis/contentauthor/app/H5PLibrary.php +++ b/sourcecode/apis/contentauthor/app/H5PLibrary.php @@ -36,6 +36,10 @@ class H5PLibrary extends Model protected $guarded = ['id']; + protected $casts = [ + 'patch_version_in_folder_name' => 'bool', + ]; + protected static function boot(): void { parent::boot(); @@ -144,23 +148,86 @@ public function getVersions($asModels = false) return $asModels !== true ? $versions : $this->hydrate($versions->toArray()); } - public function getLibraryString($folderName = false) + /** + * @param bool|null $withPatchVersion Null to use patchVersionInFolderName value to decide or true/false to force + */ + public function getLibraryString(?bool $withPatchVersion = null): string + { + return self::getLibraryName($this->getLibraryH5PFriendly(), false, $withPatchVersion); + } + + /** + * @param bool|null $withPatchVersion Null to use patchVersionInFolderName value to decide or true/false to force + */ + public function getFolderName(?bool $withPatchVersion = null): string { - return \H5PCore::libraryToString($this->getLibraryH5PFriendly(), $folderName); + return self::getLibraryName($this->getLibraryH5PFriendly(), true, $withPatchVersion); } - public function getLibraryH5PFriendly($machineName = 'name') + /** + * @param array{machineName?: string, name?: string, majorVersion: int, minorVersion: int, patchVersion: int, patchVersionInFolderName: bool} $libraryData + * @param bool|null $withPatchVersion Null to use patchVersionInFolderName value to decide or true/false to force + * @throws \InvalidArgumentException If requesting full version without patchVersion present in data + */ + public static function libraryToFolderName(array $libraryData, ?bool $withPatchVersion = null): string + { + return self::getLibraryName($libraryData, true, $withPatchVersion); + } + + /** + * @param array{machineName?: string, name?: string, majorVersion: int, minorVersion: int, patchVersion: int, patchVersionInFolderName: bool} $libraryData + * @param bool|null $withPatchVersion Null to use patchVersionInFolderName value to decide or true/false to force + * @throws \InvalidArgumentException If requesting full version without patchVersion present in data + */ + public static function libraryToString(array $libraryData, ?bool $withPatchVersion = null): string + { + return self::getLibraryName($libraryData, false, $withPatchVersion); + } + + /** + * @throws \InvalidArgumentException If requesting full version without patchVersion present in data + */ + private static function getLibraryName(array $libraryData, bool $asFolder, ?bool $withPatchVersion): string + { + $usePatch = $withPatchVersion === true || ($withPatchVersion === null && array_key_exists('patchVersionInFolderName', $libraryData) && $libraryData['patchVersionInFolderName']); + if ($usePatch && !isset($libraryData['patchVersion'])) { + throw new \InvalidArgumentException('Full version name requested but patch version missing'); + } + + if ($usePatch) { + $format = $asFolder ? '%s-%d.%d.%d' : '%s %d.%d.%d'; + } else { + $format = $asFolder ? '%s-%d.%d' : '%s %d.%d'; + } + + return sprintf( + $format, + $libraryData['machineName'] ?? $libraryData['name'], + $libraryData['majorVersion'], + $libraryData['minorVersion'], + $libraryData['patchVersion'] ?? '' + ); + } + + public function getLibraryH5PFriendly($machineName = 'name'): array { return [ 'machineName' => $this->$machineName, 'majorVersion' => $this->major_version, 'minorVersion' => $this->minor_version, + 'patchVersion' => $this->patch_version, + 'patchVersionInFolderName' => $this->patch_version_in_folder_name, ]; } public function getTitleAndVersionString() { - return \H5PCore::libraryToString($this->getLibraryH5PFriendly('title')); + return self::getLibraryName([ + 'machineName' => $this->title, + 'majorVersion' => $this->major_version, + 'minorVersion' => $this->minor_version, + 'patchVersion' => $this->patch_version, + ], false, true); } /** @@ -249,7 +316,7 @@ public function getAddons() public function supportsMaxScore(): bool { - $libraryLocation = sprintf('libraries/%s/presave.js', self::getLibraryString(true)); + $libraryLocation = sprintf('libraries/%s/presave.js', self::getFolderName()); if (Storage::disk()->exists($libraryLocation)) { return true; } diff --git a/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminH5PDetailsController.php b/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminH5PDetailsController.php index 174de114b1..c7bbe3cdb3 100644 --- a/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminH5PDetailsController.php +++ b/sourcecode/apis/contentauthor/app/Http/Controllers/Admin/AdminH5PDetailsController.php @@ -31,7 +31,7 @@ public function __construct( public function checkLibrary(H5PLibrary $library): View { - $h5pDataFolderName = $library->getLibraryString(true); + $h5pDataFolderName = $library->getFolderName(); $tmpLibrariesRelative = 'libraries'; $tmpLibraryRelative = 'libraries/' . $h5pDataFolderName; // Download files from bucket to tmp folder diff --git a/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php b/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php index 983ffd4762..381168c953 100644 --- a/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php +++ b/sourcecode/apis/contentauthor/app/Http/Controllers/H5PController.php @@ -332,7 +332,7 @@ public function edit(Request $request, int $id): View $state = H5PStateDataObject::create($displayOptions + [ 'id' => $h5pContent->id, - 'library' => $library->getLibraryString(), + 'library' => $library->getLibraryString(false), 'libraryid' => $h5pContent->library_id, 'parameters' => $params, 'language_iso_639_3' => $contentLanguage, diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php index 58aebcaad6..a53987606e 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/AjaxRequest.php @@ -259,8 +259,8 @@ private function libraryRebuild(Request $request): array $libraries = collect(); $this->getLibraryDetails($H5PLibrary, $libraries); - if ($libraries->has($H5PLibrary->getLibraryString())) { - $libraryData = $libraries->get($H5PLibrary->getLibraryString()); + if ($libraries->has($H5PLibrary->getLibraryString(false))) { + $libraryData = $libraries->get($H5PLibrary->getLibraryString(false)); if (array_key_exists('semantics', $libraryData)) { $H5PLibrary->semantics = $libraryData['semantics']; $H5PLibrary->save(); @@ -292,7 +292,7 @@ private function getLibraryDetails(H5PLibrary $H5PLibrary, Collection $affectedL { /** @var H5PValidator $validator */ $validator = resolve(H5PValidator::class); - $h5pDataFolderName = $H5PLibrary->getLibraryString(true); + $h5pDataFolderName = $H5PLibrary->getFolderName(); $tmpLibrariesRelative = 'libraries'; $tmpLibraryRelative = 'libraries/' . $h5pDataFolderName; // Download files from bucket to tmp folder @@ -304,18 +304,18 @@ private function getLibraryDetails(H5PLibrary $H5PLibrary, Collection $affectedL ); $tmpLibraries = $this->core->h5pF->getH5pPath($tmpLibrariesRelative); $tmpLibraryFolder = $this->core->h5pF->getH5pPath($tmpLibraryRelative); - $libraryData = $validator->getLibraryData($H5PLibrary->getLibraryString(true), $tmpLibraryFolder, $tmpLibraries); + $libraryData = $validator->getLibraryData($H5PLibrary->getFolderName(), $tmpLibraryFolder, $tmpLibraries); $libraryData['libraryId'] = $H5PLibrary->id; - if (!$affectedLibraries->has($H5PLibrary->getLibraryString())) { - $affectedLibraries->put($H5PLibrary->getLibraryString(), $libraryData); + if (!$affectedLibraries->has($H5PLibrary->getLibraryString(false))) { + $affectedLibraries->put($H5PLibrary->getLibraryString(false), $libraryData); } foreach (['preloadedDependencies', 'dynamicDependencies', 'editorDependencies'] as $value) { if (!empty($libraryData[$value])) { foreach ($libraryData[$value] as $library) { /** @var H5PLibrary $dependentLibrary */ $dependentLibrary = H5PLibrary::fromLibrary($library)->first(); - if (!$affectedLibraries->has($dependentLibrary->getLibraryString())) { + if (!$affectedLibraries->has($dependentLibrary->getLibraryString(false))) { $affectedLibraries = $this->getLibraryDetails($dependentLibrary, $affectedLibraries); } } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorAjax.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorAjax.php index b461aa8801..0e1a5e2731 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorAjax.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorAjax.php @@ -102,7 +102,7 @@ public function getTranslations($libraries, $language_code) }) ->get() ->mapWithKeys(function ($library) { - return [$library->library->getLibraryString() => $library->translation]; + return [$library->library->getLibraryString(false) => $library->translation]; }) ->toArray(); } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php index e35612a558..7754d1ea4c 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/EditorStorage.php @@ -65,7 +65,7 @@ public function getLibraries($libraries = null) ->map(function ($h5pLibrary) { /** @var H5PLibrary $h5pLibrary */ $library = [ - 'uberName' => $h5pLibrary->getLibraryString(), + 'uberName' => $h5pLibrary->getLibraryString(false), 'name' => $h5pLibrary->name, 'majorVersion' => $h5pLibrary->major_version, 'minorVersion' => $h5pLibrary->minor_version, @@ -101,7 +101,7 @@ public function getLibraries($libraries = null) $library->minorVersion = $library->minor_version; // Add new library - $library->uberName = $library->getLibraryString(); + $library->uberName = $library->getLibraryString(false); if ($index > 0) { $library->isOld = true; } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php index 0776f5d758..09805149d0 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Framework.php @@ -21,6 +21,7 @@ use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Request; +use Illuminate\Support\Stringable; use InvalidArgumentException; use PDO; use Psr\Http\Message\ResponseInterface; @@ -132,19 +133,35 @@ public function getPlatformInfo() ]; } - public function fetchExternalData($url, $data = null, $blocking = true, $stream = null): string|null - { + public function fetchExternalData( + $url, + $data = null, + $blocking = true, + $stream = null, + $fullData = false, + $headers = [], + $files = [], + $method = 'POST' + ): string|array|null { $method = $data ? 'POST' : 'GET'; $options = [RequestOptions::FORM_PARAMS => $data]; if ($stream !== null) { $options[RequestOptions::SINK] = $stream; } + $options[RequestOptions::HEADERS] = $headers; return $this->httpClient->requestAsync($method, $url, $options) - ->then(static function (ResponseInterface $response) use ($blocking) { + ->then(static function (ResponseInterface $response) use ($blocking, $fullData) { if (!$blocking) { return null; } + if ($fullData) { + return [ + 'status' => $response->getStatusCode(), + 'headers' => $response->getHeaders(), + 'data' => $response->getBody()->getContents(), + ]; + } return $response->getBody()->getContents(); }) @@ -490,7 +507,8 @@ public function saveLibraryData(&$libraryData, $new = true) 'metadata_settings' => $libraryData['metadataSettings'], 'add_to' => $libraryData['addTo'], 'has_icon' => $libraryData['hasIcon'] ?? 0, - 'tutorial_url' => '' + 'tutorial_url' => '', + 'patch_version_in_folder_name' => true, ]); $libraryData['libraryId'] = $h5pLibrary->id; @@ -809,6 +827,7 @@ public function loadLibrary($machineName, $majorVersion, $minorVersion): array|f 'preloadedCss' => $h5pLibrary->preloaded_css, 'dropLibraryCss' => $h5pLibrary->drop_library_css, 'semantics' => $h5pLibrary->semantics, + 'patchVersionInFolderName' => $h5pLibrary->patch_version_in_folder_name, ]; foreach ($h5pLibrary->libraries as $dependency) { @@ -816,6 +835,8 @@ public function loadLibrary($machineName, $majorVersion, $minorVersion): array|f 'machineName' => $dependency->requiredLibrary->name, 'majorVersion' => $dependency->requiredLibrary->major_version, 'minorVersion' => $dependency->requiredLibrary->minor_version, + 'patchVersion' => $dependency->requiredLibrary->patch_version, + 'patchVersionInFolderName' => $dependency->requiredLibrary->patch_version_in_folder_name, ]; } @@ -897,10 +918,11 @@ public function deleteLibrary($library): void throw new TypeError(sprintf('Expected object, %s given', get_debug_type($library))); } + /** @var H5PLibrary $libraryModel */ $libraryModel = H5PLibrary::findOrFail($library->id); $libraryModel->deleteOrFail(); - app(CerpusStorageInterface::class)->deleteLibrary($libraryModel); + app(\H5PFileStorage::class)->deleteLibrary($libraryModel->getLibraryH5PFriendly()); } /** @@ -943,6 +965,8 @@ public function loadContent($id) 'libraryName' => $h5pcontent->library->name, 'libraryMajorVersion' => $h5pcontent->library->major_version, 'libraryMinorVersion' => $h5pcontent->library->minor_version, + 'libraryPatchVersion' => $h5pcontent->library->patch_version, + 'libraryFullVersionName' => $h5pcontent->library->getLibraryString(), 'libraryEmbedTypes' => $h5pcontent->library->embed_types, 'libraryFullscreen' => $h5pcontent->library->fullscreen, 'language' => $h5pcontent->metadata->default_language ?? null, @@ -976,6 +1000,8 @@ public function loadContent($id) * - preloadedJs(optional): comma separated string with js file paths * - preloadedCss(optional): comma separated sting with css file paths * - dropCss(optional): csv of machine names + * - dependencyType: editor or preloaded + * - patchVersionInFolderName: Is patch version a part of the folder name */ public function loadContentDependencies($id, $type = null) { @@ -993,6 +1019,7 @@ public function loadContentDependencies($id, $type = null) , hl.preloaded_js AS preloadedJs , hcl.drop_css AS dropCss , hcl.dependency_type AS dependencyType + , hl.patch_version_in_folder_name AS patchVersionInFolderName FROM h5p_contents_libraries hcl JOIN h5p_libraries hl ON hcl.library_id = hl.id WHERE hcl.content_id = ?"; @@ -1324,4 +1351,27 @@ public function libraryHasUpgrade($library) $h5pLibrary = H5PLibrary::fromLibrary($library)->first(); return !is_null($h5pLibrary) && $h5pLibrary->isUpgradable(); } + + public function replaceContentHubMetadataCache($metadata, $lang) + { + // H5P Content Hub is not in use + } + + public function getContentHubMetadataCache($lang = 'en') + { + // H5P Content Hub is not in use + return new Stringable(); + } + + public function getContentHubMetadataChecked($lang = 'en') + { + // H5P Content Hub is not in use + return now()->toRfc7231String(); + } + + public function setContentHubMetadataChecked($time, $lang = 'en') + { + // H5P Content Hub is not in use + return true; + } } diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php index ef6b211808..e212c435d9 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PExport.php @@ -44,7 +44,7 @@ public function generateExport(H5PContent $content): bool $parameters = $content->parameters; } - $library = H5PCore::libraryFromString($content->library->getLibraryString()) + $library = H5PCore::libraryFromString($content->library->getLibraryString(false)) ?: throw new UnexpectedValueException('Bad library string'); $library['libraryId'] = $content->library->id; $library['name'] = $content->library->name; @@ -60,7 +60,7 @@ public function generateExport(H5PContent $content): bool // resolving dependencies, as it likes to corrupt the data $validatorParams = (object)[ - 'library' => $content->library->getLibraryString(), + 'library' => $content->library->getLibraryString(false), 'params' => json_decode($content->parameters) ]; $this->validator->validateLibrary($validatorParams, (object) [ diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PViewConfig.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PViewConfig.php index 0aaee9ae4a..581d16c676 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PViewConfig.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/H5PViewConfig.php @@ -76,7 +76,7 @@ public function loadContent(int $id): static ); $this->contentConfig['exportUrl'] = route('content-download', ['h5p' => $this->content['id']]); - $this->contentConfig['library'] = $this->h5pCore->libraryToString($this->content['library']); + $this->contentConfig['library'] = $this->content['libraryFullVersionName']; $this->contentConfig['fullScreen'] = $this->content['library']['fullscreen']; $this->contentConfig['title'] = $this->content['title']; $this->contentConfig['metadata'] = $this->content['metadata']; diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Interfaces/CerpusStorageInterface.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Interfaces/CerpusStorageInterface.php index 546efea1f4..643768c6fd 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Interfaces/CerpusStorageInterface.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Interfaces/CerpusStorageInterface.php @@ -2,8 +2,6 @@ namespace App\Libraries\H5P\Interfaces; -use App\H5PLibrary; - interface CerpusStorageInterface { public function getDisplayPath(bool $fullUrl = true); @@ -18,8 +16,6 @@ public function getAjaxPath(); public function alterLibraryFiles($files); - public function deleteLibrary(H5PLibrary $library); - public function getFileUrl(string $path); /** diff --git a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php index eb3429071f..5e8c02192c 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php +++ b/sourcecode/apis/contentauthor/app/Libraries/H5P/Storage/H5PCerpusStorage.php @@ -107,7 +107,8 @@ public function cloneContentFile($file, $fromId, $toId) */ public function saveLibrary($library) { - $path = sprintf(ContentStorageSettings::LIBRARY_PATH, \H5PCore::libraryToString($library, true)); + $library['patchVersionInFolderName'] = true; + $path = sprintf(ContentStorageSettings::LIBRARY_PATH, \H5PCore::libraryToFolderName($library)); $libraryPath = Str::after($library['uploadDirectory'], $this->uploadDisk->path("")); $this->deleteLibraryFromPath($path); @@ -236,9 +237,11 @@ public function exportContent($id, $target) */ public function exportLibrary($library, $target) { - $folder = \H5PCore::libraryToString($library, true); + $folder = H5PLibrary::libraryToFolderName($library); + // To make the exported file backward compatible, we don't use patch in target folder name + $targetFolder = H5PLibrary::libraryToFolderName($library, false); $srcPath = sprintf(ContentStorageSettings::LIBRARY_PATH, $folder); - $finalTarget = Str::after($target, $this->uploadDisk->path("")) . "/$folder"; + $finalTarget = Str::after($target, $this->uploadDisk->path("")) . "/$targetFolder"; if ($this->hasLibraryVersion($folder, sprintf(ContentStorageSettings::LIBRARY_VERSION_PREFIX, $library['majorVersion'], $library['minorVersion'], $library['patchVersion']))) { $this->exportLocalDirectory($srcPath, $finalTarget, $folder); } else { @@ -494,11 +497,10 @@ public function hasPresave($libraryName, $developmentPath = null) */ public function getUpgradeScript($machineName, $majorVersion, $minorVersion) { - $path = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, \H5PCore::libraryToString([ - 'machineName' => $machineName, - 'majorVersion' => $majorVersion, - 'minorVersion' => $minorVersion, - ], true)); + /** @var H5PLibrary $library */ + $library = H5PLibrary::fromLibrary([$machineName, $majorVersion, $minorVersion])->latestVersion()->first(); + $path = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, $library->getFolderName()); + return $this->filesystem->exists($path) ? "/$path" : null; } @@ -601,11 +603,12 @@ private function hasLibraryVersion($path, $versionString): bool ->isNotEmpty(); } - public function deleteLibrary(H5PLibrary $library) + public function deleteLibrary($library) { - $libraryPath = sprintf(ContentStorageSettings::LIBRARY_PATH, $library->getLibraryString(true)); + $libraryPath = sprintf(ContentStorageSettings::LIBRARY_PATH, \H5PCore::libraryToFolderName($library)); $deleteRemote = $this->deleteLibraryFromPath($libraryPath); $deleteLocal = $this->uploadDisk->exists($libraryPath) ? $this->uploadDisk->deleteDirectory($libraryPath) : true; + return $deleteRemote && $deleteLocal; } diff --git a/sourcecode/apis/contentauthor/composer.json b/sourcecode/apis/contentauthor/composer.json index be9b5acc0e..d68e52ef1d 100644 --- a/sourcecode/apis/contentauthor/composer.json +++ b/sourcecode/apis/contentauthor/composer.json @@ -27,8 +27,8 @@ "embed/embed": "^3.2", "firebase/php-jwt": "^6.3", "guzzlehttp/guzzle": "^7.4", - "h5p/h5p-core": "^1.24", - "h5p/h5p-editor": "dev-master#1ae19fdb80839b32dad3846d6b0a5c745f8f6187", + "h5p/h5p-core": "dev-master#0a82667e00175dea55e37ad8bbebedbfed25b5b6", + "h5p/h5p-editor": "dev-master#0365b081efa8b55ab9fd58594aa599f9630268f6", "laravel/framework": "^9.17", "laravel/horizon": "^5.7", "laravel/tinker": "^2.6", diff --git a/sourcecode/apis/contentauthor/composer.lock b/sourcecode/apis/contentauthor/composer.lock index b4762e51d0..caad9f3bc7 100644 --- a/sourcecode/apis/contentauthor/composer.lock +++ b/sourcecode/apis/contentauthor/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f8f90b383667fa459cb93c1d1c6f8360", + "content-hash": "43a247439a9ee7a8b7897e4ede814bcd", "packages": [ { "name": "auth0/auth0-php", @@ -2463,21 +2463,22 @@ }, { "name": "h5p/h5p-core", - "version": "1.24.3", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/h5p/h5p-php-library.git", - "reference": "db3da7a1441ae6c9ffacbb8110f41758c85beaa0" + "reference": "0a82667e00175dea55e37ad8bbebedbfed25b5b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/h5p/h5p-php-library/zipball/db3da7a1441ae6c9ffacbb8110f41758c85beaa0", - "reference": "db3da7a1441ae6c9ffacbb8110f41758c85beaa0", + "url": "https://api.github.com/repos/h5p/h5p-php-library/zipball/0a82667e00175dea55e37ad8bbebedbfed25b5b6", + "reference": "0a82667e00175dea55e37ad8bbebedbfed25b5b6", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.0.0" }, + "default-branch": true, "type": "library", "autoload": { "files": [ @@ -2518,9 +2519,9 @@ ], "support": { "issues": "https://github.com/h5p/h5p-php-library/issues", - "source": "https://github.com/h5p/h5p-php-library/tree/wp-1.15.3" + "source": "https://github.com/h5p/h5p-php-library/tree/master" }, - "time": "2021-04-22T09:30:45+00:00" + "time": "2023-06-27T13:26:01+00:00" }, { "name": "h5p/h5p-editor", @@ -2528,12 +2529,12 @@ "source": { "type": "git", "url": "https://github.com/h5p/h5p-editor-php-library.git", - "reference": "1ae19fdb80839b32dad3846d6b0a5c745f8f6187" + "reference": "0365b081efa8b55ab9fd58594aa599f9630268f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/h5p/h5p-editor-php-library/zipball/1ae19fdb80839b32dad3846d6b0a5c745f8f6187", - "reference": "1ae19fdb80839b32dad3846d6b0a5c745f8f6187", + "url": "https://api.github.com/repos/h5p/h5p-editor-php-library/zipball/0365b081efa8b55ab9fd58594aa599f9630268f6", + "reference": "0365b081efa8b55ab9fd58594aa599f9630268f6", "shasum": "" }, "require": { @@ -2582,7 +2583,7 @@ "issues": "https://github.com/h5p/h5p-editor-php-library/issues", "source": "https://github.com/h5p/h5p-editor-php-library/tree/master" }, - "time": "2022-06-02T06:11:10+00:00" + "time": "2023-03-09T14:54:57+00:00" }, { "name": "kamermans/guzzle-oauth2-subscriber", @@ -5992,20 +5993,20 @@ }, { "name": "ramsey/uuid", - "version": "4.7.3", + "version": "4.7.4", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "433b2014e3979047db08a17a205f410ba3869cf2" + "reference": "60a4c63ab724854332900504274f6150ff26d286" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/433b2014e3979047db08a17a205f410ba3869cf2", - "reference": "433b2014e3979047db08a17a205f410ba3869cf2", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/60a4c63ab724854332900504274f6150ff26d286", + "reference": "60a4c63ab724854332900504274f6150ff26d286", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11", "ext-json": "*", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" @@ -6068,7 +6069,7 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.3" + "source": "https://github.com/ramsey/uuid/tree/4.7.4" }, "funding": [ { @@ -6080,7 +6081,7 @@ "type": "tidelift" } ], - "time": "2023-01-12T18:13:24+00:00" + "time": "2023-04-15T23:01:58+00:00" }, { "name": "spatie/backtrace", @@ -11724,6 +11725,7 @@ "stability-flags": { "cerpus/edlib-resource-kit": 20, "cerpus/edlib-resource-kit-laravel": 20, + "h5p/h5p-core": 20, "h5p/h5p-editor": 20 }, "prefer-stable": false, diff --git a/sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php b/sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php new file mode 100644 index 0000000000..51b607d866 --- /dev/null +++ b/sourcecode/apis/contentauthor/database/migrations/2023_04_25_065450_add_patch_version_in_folder_name_to_h5p_libraries_table.php @@ -0,0 +1,31 @@ +boolean('patch_version_in_folder_name')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('h5p_libraries', function (Blueprint $table) { + $table->dropColumn('patch_version_in_folder_name'); + }); + } +}; diff --git a/sourcecode/apis/contentauthor/public/js/h5p/core-override/h5p-content-type.js b/sourcecode/apis/contentauthor/public/js/h5p/core-override/h5p-content-type.js new file mode 100755 index 0000000000..1ce6e1dbc5 --- /dev/null +++ b/sourcecode/apis/contentauthor/public/js/h5p/core-override/h5p-content-type.js @@ -0,0 +1,41 @@ +/** + * H5P.ContentType is a base class for all content types. Used by newRunnable() + * + * Functions here may be overridable by the libraries. In special cases, + * it is also possible to override H5P.ContentType on a global level. + * + * NOTE that this doesn't actually 'extend' the event dispatcher but instead + * it creates a single instance which all content types shares as their base + * prototype. (in some cases this may be the root of strange event behavior) + * + * @class + * @augments H5P.EventDispatcher + */ +H5P.ContentType = function (isRootLibrary) { + + function ContentType() {} + + // Inherit from EventDispatcher. + ContentType.prototype = new H5P.EventDispatcher(); + + /** + * Is library standalone or not? Not beeing standalone, means it is + * included in another library + * + * @return {Boolean} + */ + ContentType.prototype.isRoot = function () { + return isRootLibrary; + }; + + /** + * Returns the file path of a file in the current library + * @param {string} filePath The path to the file relative to the library folder + * @return {string} The full path to the file + */ + ContentType.prototype.getLibraryFilePath = function (filePath) { + return H5P.getLibraryPath(this.libraryInfo.versionedName.replace(' ', '-')) + '/' + filePath; + }; + + return ContentType; +}; diff --git a/sourcecode/apis/contentauthor/public/js/h5p/request-queue.js b/sourcecode/apis/contentauthor/public/js/h5p/core-override/request-queue.js old mode 100644 new mode 100755 similarity index 100% rename from sourcecode/apis/contentauthor/public/js/h5p/request-queue.js rename to sourcecode/apis/contentauthor/public/js/h5p/core-override/request-queue.js diff --git a/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core-bundle.js b/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core-bundle.js index 04daeb7a66..00b4d45b95 100644 --- a/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core-bundle.js +++ b/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core-bundle.js @@ -3,7 +3,8 @@ import '../../../vendor/h5p/h5p-core/js/h5p.js'; import '../../../vendor/h5p/h5p-core/js/h5p-event-dispatcher.js'; import '../../../vendor/h5p/h5p-core/js/h5p-x-api-event.js'; import '../../../vendor/h5p/h5p-core/js/h5p-x-api.js'; -import '../../../vendor/h5p/h5p-core/js/h5p-content-type.js'; +import '../../../public/js/h5p/core-override/h5p-content-type.js'; //TODO Replaced to support patch-version in library folder name. Used by libraries to loads assets that is not js or css import '../../../vendor/h5p/h5p-core/js/h5p-confirmation-dialog.js'; -import '../../../public/js/h5p/request-queue.js'; //TODO Change to vanilla H5P when they fix import '../../../vendor/h5p/h5p-core/js/h5p-action-bar.js'; +import '../../../public/js/h5p/core-override/request-queue.js'; //TODO Change to vanilla H5P when they fix https://github.com/h5p/h5p-php-library/pull/66 +import '../../../vendor/h5p/h5p-core/js/h5p-tooltip.js'; diff --git a/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core.scss b/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core.scss index f05a95dcfa..10a43c61bf 100644 --- a/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core.scss +++ b/sourcecode/apis/contentauthor/resources/assets/entrypoints/h5p-core.scss @@ -1,3 +1,4 @@ @import '../../../vendor/h5p/h5p-core/styles/h5p.css'; @import '../../../vendor/h5p/h5p-core/styles/h5p-confirmation-dialog.css'; @import '../../../vendor/h5p/h5p-core/styles/h5p-core-button.css'; +@import '../../../vendor/h5p/h5p-core/styles/h5p-tooltip.css'; diff --git a/sourcecode/apis/contentauthor/resources/views/h5p/show.blade.php b/sourcecode/apis/contentauthor/resources/views/h5p/show.blade.php index a485e9580c..e1629114fb 100644 --- a/sourcecode/apis/contentauthor/resources/views/h5p/show.blade.php +++ b/sourcecode/apis/contentauthor/resources/views/h5p/show.blade.php @@ -14,7 +14,7 @@ @foreach( $styles as $css) {!! HTML::style($css) !!} @endforeach - {!! HTML::script('https://code.jquery.com/jquery-1.11.3.min.js') !!} + {!! HTML::script('https://code.jquery.com/jquery-1.12.4.min.js') !!} diff --git a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleLockTest.php b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleLockTest.php index eaf17ec16a..f2be17c291 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleLockTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleLockTest.php @@ -33,6 +33,7 @@ public function setUp(): void $versionData = new VersionData(); $this->setupVersion([ 'createVersion' => $versionData->populate((object) ['id' => $this->faker->uuid]), + 'getVersion' => $versionData->populate((object) ['id' => $this->faker->uuid]), ]); } diff --git a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleTest.php b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleTest.php index 33af3dcb4c..a20251e6ec 100755 --- a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleTest.php @@ -132,7 +132,7 @@ public function testCreateArticleWithMathContent() public function testCreateAndEditArticleWithIframeContent() { - $this->setupVersion(); + $this->setupVersion(['getVersion' => false]); Event::fake(); $authId = Str::uuid(); @@ -175,7 +175,7 @@ public function testCreateAndEditArticleWithIframeContent() public function testEditArticle() { - $this->setupVersion(); + $this->setupVersion(['getVersion' => false]); $this->setupAuthApi([ 'getUser' => new User("1", "this", "that", "this@that.com") ]); @@ -218,7 +218,7 @@ public function testEditArticle() public function testEditArticleWithDraftEnabled() { - $this->setupVersion(); + $this->setupVersion(['getVersion' => false]); $this->setupAuthApi([ 'getUser' => new User("1", "this", "that", "this@that.com") ]); @@ -285,7 +285,7 @@ public function testEditArticleWithDraftEnabled() public function testViewArticle() { - $this->setupVersion(); + $this->setupVersion(['getVersion' => false]); /** @var Article $article */ $article = Article::factory()->create([ diff --git a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php index d63fa64eac..f31c0c9a01 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Article/ArticleVersioningTest.php @@ -33,6 +33,7 @@ public function setUp(): void $versionData = new VersionData(); $this->setupVersion([ 'createVersion' => $versionData->populate((object) ['id' => $this->faker->uuid]), + 'getVersion' => $versionData->populate((object) ['id' => $this->faker->uuid]), ]); } diff --git a/sourcecode/apis/contentauthor/tests/Integration/H5PContentTest.php b/sourcecode/apis/contentauthor/tests/Integration/H5PContentTest.php new file mode 100644 index 0000000000..2ae26ac0b9 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/H5PContentTest.php @@ -0,0 +1,64 @@ +create([ + 'has_icon' => 1, + 'patch_version_in_folder_name' => $usePatch, + ]); + + $frameWork = $this->createMock(\H5PFrameworkInterface::class); + $this->instance(\H5PFrameworkInterface::class, $frameWork); + + $frameWork + ->expects($this->once()) + ->method('getLibraryFileUrl') + ->with($expectedPath, 'icon.svg') + ->willReturn("assets/$expectedPath/icon.svg"); + + $result = H5PContent::getContentTypeInfo('H5P.Foobar'); + $this->assertInstanceOf(ContentTypeDataObject::class, $result); + $this->assertSame('H5P.Foobar', $result->contentType); + $this->assertSame($library->title, $result->title); + $this->assertSame("assets/$expectedPath/icon.svg", $result->icon); + } + + public function provider_getContentTypeInfo(): \Generator + { + yield [false, 'H5P.Foobar-1.2']; + yield [true, 'H5P.Foobar-1.2.3']; + } + + public function test_getContentTypeInfo_NoIcon(): void + { + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create(); + + $frameWork = $this->createMock(\H5PFrameworkInterface::class); + $this->instance(\H5PFrameworkInterface::class, $frameWork); + + $frameWork + ->expects($this->never()) + ->method('getLibraryFileUrl'); + + $result = H5PContent::getContentTypeInfo('H5P.Foobar'); + $this->assertInstanceOf(ContentTypeDataObject::class, $result); + $this->assertSame('H5P.Foobar', $result->contentType); + $this->assertSame($library->title, $result->title); + $this->assertSame('http://localhost/graphical/h5p_logo.svg', $result->icon); + } +} diff --git a/sourcecode/apis/contentauthor/tests/Integration/H5PLibrariesHubCacheTest.php b/sourcecode/apis/contentauthor/tests/Integration/H5PLibrariesHubCacheTest.php new file mode 100644 index 0000000000..d72ec0cc4d --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/H5PLibrariesHubCacheTest.php @@ -0,0 +1,32 @@ + 'H5P.Foobar', + 'major_version' => 1, + 'minor_version' => 2, + 'patch_version_in_folder_name' => $usePatch, + ]); + + $this->assertSame($expected, $lib->getLibraryString($isFolder)); + } + + public function provider_LibraryString(): \Generator + { + yield [true, false, 'H5P.Foobar-1.2']; + yield [true, true, 'H5P.Foobar-1.2']; + yield [false, false, 'H5P.Foobar 1.2']; + yield [false, true, 'H5P.Foobar 1.2']; + } +} diff --git a/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php b/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php index 48878700cc..3d80f89e3e 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Http/Controllers/H5PControllerTest.php @@ -30,8 +30,8 @@ class H5PControllerTest extends TestCase use RefreshDatabase; use MockAuthApi; - /** @dataProvider provider_adapterMode */ - public function testCreate(string $adapterMode): void + /** @dataProvider provider_testCreate */ + public function testCreate(string $adapterMode, ?string $contentType): void { $faker = Factory::create(); $this->session([ @@ -49,11 +49,15 @@ public function testCreate(string $adapterMode): void 'redirectToken' => $faker->uuid, ]); + H5PLibrary::factory()->create(); + /** @var H5PCore $h5pCore */ $h5pCore = app(H5pCore::class); + /** @var H5PController $articleController */ $articleController = app(H5PController::class); - $result = $articleController->create($request, $h5pCore); + $result = $articleController->create($request, $h5pCore, $contentType); + $this->assertInstanceOf(View::class, $result); $data = $result->getData(); @@ -67,9 +71,14 @@ public function testCreate(string $adapterMode): void $this->assertNotEmpty($data['editorSetup']); $this->assertNotEmpty($data['state']); $this->assertArrayHasKey('configJs', $data); + $this->assertSame($contentType, $data['libName']); $config = json_decode(substr($result['config'], 25, -9), true, flags: JSON_THROW_ON_ERROR); - $this->assertTrue($config['hubIsEnabled']); + if ($contentType === null) { + $this->assertTrue($config['hubIsEnabled']); + } else { + $this->assertFalse($config['hubIsEnabled']); + } $this->assertEmpty($config['contents']); $this->assertSame('nb-no', $config['locale']); $this->assertSame('nb', $config['localeConverted']); @@ -90,10 +99,11 @@ public function testCreate(string $adapterMode): void $this->assertEquals('Emily Quackfaster', $editorSetup['creatorName']); $state = json_decode($data['state'], true, flags: JSON_THROW_ON_ERROR); + $this->assertNull($state['id']); $this->assertNull($state['title']); $this->assertFalse($state['isPublished']); - $this->assertNull($state['library']); + $this->assertSame($contentType, $state['library']); $this->assertNull($state['libraryid']); $this->assertSame('nob', $state['language_iso_639_3']); $this->assertEquals(config('license.default-license'), $state['license']); @@ -106,6 +116,14 @@ public function testCreate(string $adapterMode): void } } + public function provider_testCreate(): \Generator + { + yield 'cerpus-withoutContentType' => ['cerpus', null]; + yield 'ndla-withoutContentType' => ['ndla', null]; + yield 'cerpus-withContentType' => ['cerpus', 'H5P.Toolbar 1.2']; + yield 'ndla-withContentType' => ['ndla', 'H5P.Toolbar 1.2']; + } + /** @dataProvider provider_adapterMode */ public function testEdit(string $adapterMode): void { @@ -201,6 +219,7 @@ public function testEdit(string $adapterMode): void $this->assertNotEmpty($config['editor']['ajaxPath']); $editorSetup = json_decode($data['editorSetup'], true, flags: JSON_THROW_ON_ERROR); + $this->assertEquals($lib->title . ' 1.6.3', $editorSetup['contentProperties']['type']); $this->assertEquals('Emily Quackfaster', $editorSetup['contentProperties']['ownerName']); $this->assertSame($upgradeLib->id, $editorSetup['libraryUpgradeList'][0]['id']); $this->assertSame('nb', $editorSetup['h5pLanguage']); diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/API/H5PImportControllerTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/API/H5PImportControllerTest.php index fd942a2cbf..f746f51fe7 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/API/H5PImportControllerTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/API/H5PImportControllerTest.php @@ -121,7 +121,7 @@ public function importH5P() 'title' => $title, 'library_id' => $library->id, ]); - $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getLibraryString(true)))); + $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getFolderName()))); /** @var H5PContent $h5pContent */ $h5pContent = H5PContent::with('metadata') @@ -174,7 +174,7 @@ public function importH5PWithImage() 'title' => $title, 'library_id' => $library->id, ]); - $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getLibraryString(true)))); + $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getFolderName()))); $h5pContent = H5PContent::with('metadata') ->where('title', $title) @@ -230,7 +230,7 @@ public function importH5PWithMetadata() 'title' => $title, 'library_id' => $library->id, ]); - $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getLibraryString(true)))); + $this->assertFileExists($this->fakeDisk->path(sprintf("libraries/%s/semantics.json", $library->getFolderName()))); $h5pContent = H5PContent::with('metadata') ->where('title', $title) diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php new file mode 100644 index 0000000000..ba807412a1 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/AjaxRequestTest.php @@ -0,0 +1,64 @@ +create(); + /** @var H5PLibrary $preLib */ + $preLib = H5PLibrary::factory()->create(['name' => 'player', 'major_version' => 3, 'minor_version' => 14]); + /** @var H5PLibrary $dynLib */ + $dynLib = H5PLibrary::factory()->create(['name' => 'H5P.Dynamic', 'major_version' => 2, 'minor_version' => 42, 'patch_version' => 3, 'patch_version_in_folder_name' => true]); + /** @var H5PLibrary $edLib */ + $edLib = H5PLibrary::factory()->create(['name' => 'FontOk', 'major_version' => 1, 'minor_version' => 3]); + + $this->assertDatabaseEmpty('h5p_libraries_libraries'); + + $validator = $this->createMock(\H5PValidator::class); + $this->instance(\H5PValidator::class, $validator); + $validator + ->expects($this->exactly(4)) + ->method('getLibraryData') + ->withConsecutive(['H5P.Foobar-1.2'], ['player-3.14'], ['H5P.Dynamic-2.42.3'], ['FontOk-1.3']) + ->willReturnOnConsecutiveCalls([ + 'preloadedDependencies' => [$preLib->getLibraryH5PFriendly()], + 'dynamicDependencies' => [$dynLib->getLibraryH5PFriendly()], + 'editorDependencies' => [$edLib->getLibraryH5PFriendly()], + ], [], [], []); + + $this + ->withSession(['isAdmin' => true]) + ->post('/ajax', ['action' => AjaxRequest::LIBRARY_REBUILD, 'libraryId' => $library->id]) + ->assertOk() + ->assertJson([ + 'success' => true, + 'message' => 'Library rebuild', + ]); + + $this->assertDatabaseHas('h5p_libraries_libraries', [ + 'library_id' => $library->id, + 'required_library_id' => $preLib->id, + 'dependency_type' => 'preloaded', + ]); + $this->assertDatabaseHas('h5p_libraries_libraries', [ + 'library_id' => $library->id, + 'required_library_id' => $dynLib->id, + 'dependency_type' => 'dynamic', + ]); + $this->assertDatabaseHas('h5p_libraries_libraries', [ + 'library_id' => $library->id, + 'required_library_id' => $edLib->id, + 'dependency_type' => 'editor', + ]); + } +} diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php index 35ac4e648f..b6e94c1ec1 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/CRUTest.php @@ -496,6 +496,7 @@ public function enabledUserPublish_NotOwner() /** @var H5PContent $newContent */ $newContent = $contents->first(); + /** @var H5PLibrary $library */ $library = $newContent->library()->first(); $this->setupH5PAdapter([ @@ -513,7 +514,7 @@ public function enabledUserPublish_NotOwner() '_token' => csrf_token(), 'title' => 'New resource', 'action' => 'create', - 'library' => $library->getLibraryString(), + 'library' => $library->getLibraryString(false), 'parameters' => '{"params":{"simpleTest":"SimpleTest"},"metadata":{}}', 'license' => "PRIVATE", 'lti_message_type' => $this->faker->word, @@ -536,7 +537,7 @@ public function enabledUserPublish_NotOwner() ->put(route('h5p.update', $newContent->id), [ '_token' => csrf_token(), 'title' => $newContent->title, - 'library' => $library->getLibraryString(), + 'library' => $library->getLibraryString(false), 'parameters' => '{"params":{"simpleTest":"SimpleTest"},"metadata":{}}', 'license' => "PRIVATE", 'lti_message_type' => $this->faker->word, diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php new file mode 100644 index 0000000000..4be0882b9f --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorAjaxTest.php @@ -0,0 +1,74 @@ +create(); + /** @var H5PLibrary $libTest */ + $libTest = H5PLibrary::factory()->create([ + 'name' => 'H5P.UnitTest', + 'major_version' => 3, + 'minor_version' => 14, + 'patch_version' => 42, + 'patch_version_in_folder_name' => 1, + ]); + + H5PLibraryLanguage::create([ + 'library_id' => $libFoo->id, + 'language_code' => 'nb', + 'translation' => json_encode(['lib' => $libFoo->getLibraryString(false), 'lang' => 'nb']), + ]); + + H5PLibraryLanguage::create([ + 'library_id' => $libTest->id, + 'language_code' => 'nb', + 'translation' => json_encode(['lib' => $libTest->getLibraryString(false), 'lang' => 'nb']), + ]); + + H5PLibraryLanguage::create([ + 'library_id' => $libFoo->id, + 'language_code' => 'nn', + 'translation' => json_encode(['lib' => $libFoo->getLibraryString(false), 'lang' => 'nn']), + ]); + + H5PLibraryLanguage::create([ + 'library_id' => $libTest->id, + 'language_code' => 'en', + 'translation' => json_encode(['lib' => $libTest->getLibraryString(false), 'lang' => 'en']), + ]); + + $translations = (new EditorAjax())->getTranslations( + [ + $libFoo->getLibraryString(false), + $libTest->getLibraryString(false), + ], + 'nb' + ); + + $this->assertIsArray($translations); + + $this->assertArrayHasKey($libFoo->getLibraryString(false), $translations); + $data = json_decode($translations[$libFoo->getLibraryString(false)], true, JSON_THROW_ON_ERROR); + $this->assertSame($libFoo->getLibraryString(false), $data['lib']); + $this->assertSame('nb', $data['lang']); + + $this->assertArrayHasKey($libFoo->getLibraryString(false), $translations); + $data = json_decode($translations[$libTest->getLibraryString(false)], true, JSON_THROW_ON_ERROR); + $this->assertSame($libTest->getLibraryString(false), $data['lib']); + $this->assertSame('nb', $data['lang']); + } +} diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php new file mode 100644 index 0000000000..3f5bf69cc4 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/EditorStorageTest.php @@ -0,0 +1,58 @@ +create(['semantics' => 'something']); + $libraries = (object) [ + 'uberName' => $library->getLibraryString(false), + 'name' => $library->name, + 'majorVersion' => $library->major_version, + 'minorVersion' => $library->minor_version, + ]; + + $core = $this->createMock(\H5PCore::class); + $this->instance(\H5PCore::class, $core); + + /** @var EditorStorage $editorStorage */ + $editorStorage = app(EditorStorage::class); + $ret = $editorStorage->getLibraries([$libraries]); + + $this->assertCount(1, $ret); + $this->assertSame($library->id, $ret[0]->id); + $this->assertSame('H5P.Foobar 1.2', $ret[0]->uberName); + } + + public function test_getLibrary_allLibraries(): void + { + /** @var H5PLibrary $lib1 */ + $lib1 = H5PLibrary::factory()->create(['semantics' => 'something']); + /** @var H5PLibrary $lib2 */ + $lib2 = H5PLibrary::factory()->create(['name' => 'H5P.Headphones', 'semantics' => 'something']); + + $core = $this->createMock(\H5PCore::class); + $this->instance(\H5PCore::class, $core); + + /** @var EditorStorage $editorStorage */ + $editorStorage = app(EditorStorage::class); + $ret = $editorStorage->getLibraries(); + + $this->assertCount(2, $ret); + $this->assertSame($lib1->id, $ret[0]->id); + $this->assertSame($lib2->id, $ret[1]->id); + + $this->assertSame('H5P.Foobar 1.2', $ret[0]->uberName); + $this->assertSame('H5P.Headphones 1.2', $ret[1]->uberName); + } +} diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php new file mode 100644 index 0000000000..2c35da2c6d --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/FrameworkTest.php @@ -0,0 +1,206 @@ + */ + private ArrayObject $history; + + private Framework $framework; + + private MockHandler $mockedResponses; + + protected function setUp(): void + { + parent::setUp(); + + $this->history = new ArrayObject(); + $this->mockedResponses = new MockHandler(); + + $handler = HandlerStack::create($this->mockedResponses); + $handler->push(Middleware::history($this->history)); + + $client = new Client(['handler' => $handler]); + + $this->framework = new Framework( + $client, + $this->createMock(PDO::class), + $this->createMock(Filesystem::class), + ); + } + + public function testSaveLibrary(): void + { + $input = [ + 'machineName' => 'H5P.UnitTest', + 'title' => 'Unit Test', + 'majorVersion' => 2, + 'minorVersion' => 4, + 'patchVersion' => 6, + 'runnable' => 1, + 'metadataSettings' => 'Yupp', + 'addTo' => ['machineName' => 'Something'], + 'hasIcon' => 1, + 'embedTypes' => ['E1', 'E2'], + 'preloadedJs' => [ + ['path' => 'PJ1', 'name' => 'PJ1 name', 'machineName' => 'H5P.Pj1'], + ['path' => 'PJ2', 'name' => 'PJ2 name', 'machineName' => 'H5P.Pj2'], + ], + 'preloadedCss' => [ + ['path' => 'PC1', 'name' => 'PC1 name', 'machineName' => 'H5P.Pc1'], + ['path' => 'PC2', 'name' => 'PC2 name', 'machineName' => 'H5P.Pc1'], + ], + 'dropLibraryCss' => [ + ['path' => 'DC1', 'name' => 'DC1 name', 'machineName' => 'H5P.Dc1'], + ['path' => 'DC2', 'name' => 'DC2 name', 'machineName' => 'H5P.Dc2'], + ], + 'language' => [ + 'nb' => 'Norsk Bokmål', + 'nn' => 'Norsk Nynorsk', + ], + ]; + $this->framework->saveLibraryData($input); + + $this->assertDatabaseHas('h5p_libraries', ['id' => $input['libraryId']]); + $this->assertDatabaseHas('h5p_libraries_languages', [ + 'library_id' => $input['libraryId'], + 'language_code' => 'nb', + 'translation' => 'Norsk Bokmål', + ]); + $this->assertDatabaseHas('h5p_libraries_languages', [ + 'library_id' => $input['libraryId'], + 'language_code' => 'nn', + 'translation' => 'Norsk Nynorsk', + ]); + + /** @var H5PLibrary $library */ + $library = H5PLibrary::find($input['libraryId']); + + $this->assertSame('H5P.UnitTest', $library->name); + $this->assertSame('Unit Test', $library->title); + $this->assertSame(2, $library->major_version); + $this->assertSame(4, $library->minor_version); + $this->assertSame(6, $library->patch_version); + $this->assertSame(1, $library->runnable); + $this->assertSame(0, $library->fullscreen); + $this->assertSame('E1, E2', $library->embed_types); + $this->assertSame('PJ1, PJ2', $library->preloaded_js); + $this->assertSame('PC1, PC2', $library->preloaded_css); + $this->assertSame('H5P.Dc1, H5P.Dc2', $library->drop_library_css); + $this->assertSame('', $library->semantics); + $this->assertSame(1, $library->has_icon); + $this->assertSame(true, $library->patch_version_in_folder_name); + } + + public function testLoadLibrary(): void + { + H5PLibrary::factory()->create([ + 'major_version' => 1, + 'minor_version' => 1, + 'patch_version' => 9, + ]); + H5PLibrary::factory()->create([ + 'major_version' => 1, + 'minor_version' => 2, + 'patch_version' => 2, + ]); + /** @var H5PLibrary $editDep */ + $editDep = H5PLibrary::factory()->create([ + 'name' => 'H5PEditor.Foobar', + 'patch_version_in_folder_name' => true, + ]); + /** @var H5PLibrary $saved */ + $saved = H5PLibrary::factory()->create([ + 'patch_version_in_folder_name' => true, + ]); + H5PLibraryLibrary::create([ + 'library_id' => $saved->id, + 'required_library_id' => $editDep->id, + 'dependency_type' => 'editor', + ]); + + $library = $this->framework->loadLibrary('H5P.Foobar', 1, 2); + $this->assertSame($saved->id, $library['libraryId']); + $this->assertSame($saved->name, $library['machineName']); + $this->assertSame($saved->major_version, $library['majorVersion']); + $this->assertSame($saved->minor_version, $library['minorVersion']); + $this->assertSame($saved->patch_version, $library['patchVersion']); + $this->assertSame($saved->patch_version_in_folder_name, $library['patchVersionInFolderName']); + + $this->assertSame($editDep->name, $library['editorDependencies'][0]['machineName']); + $this->assertSame($editDep->patch_version_in_folder_name, $library['editorDependencies'][0]['patchVersionInFolderName']); + } + + /** @dataProvider provider_usePatch */ + public function test_deleteLibrary($usePatch): void + { + $disk = Storage::fake(); + $caStorage = App(ContentAuthorStorage::class); + $tmpDisk = Storage::fake($caStorage->getH5pTmpDiskName()); + + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); + $path = 'libraries/' . $library->getFolderName(); + + $this->assertFalse($disk->exists($path)); + $disk->put($path . '/library.json', 'just testing'); + $this->assertTrue($disk->exists($path . '/library.json')); + + $this->assertFalse($tmpDisk->exists($path)); + $tmpDisk->put($path . '/library.json', 'just testing'); + $this->assertTrue($tmpDisk->exists($path . '/library.json')); + + $lib = ['id' => $library->id]; + $this->assertDatabaseHas('h5p_libraries', $lib); + $this->framework->deleteLibrary((object) $lib); + $this->assertDatabaseMissing('h5p_libraries', $lib); + + $this->assertFalse($disk->exists($path)); + $this->assertFalse($tmpDisk->exists($path)); + } + + /** @dataProvider provider_usePatch */ + public function test_loadContent($usePatch): void + { + /** @var H5PLibrary $h5pLibrary */ + $h5pLibrary = H5PLibrary::factory()->create(['patch_version_in_folder_name' => $usePatch]); + /** @var H5PContent $h5pContent */ + $h5pContent = H5PContent::factory()->create(['library_id' => $h5pLibrary->id]); + + $content = $this->framework->loadContent($h5pContent->id); + + $this->assertSame($h5pContent->id, $content['id']); + $this->assertSame($h5pContent->id, $content['contentId']); + $this->assertSame($h5pLibrary->id, $content['libraryId']); + $this->assertSame($h5pLibrary->getLibraryString(), $content['libraryFullVersionName']); + } + + public function provider_usePatch(): \Generator + { + yield [false]; + yield [true]; + } +} diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php index 7a58ed6518..27380ecd7d 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/H5PExportTest.php @@ -3,6 +3,7 @@ namespace Tests\Integration\Libraries\H5P; use App\H5PContent; +use App\H5PLibrary; use App\Libraries\DataObjects\ContentStorageSettings; use App\Libraries\H5P\H5PExport; use App\Libraries\H5P\Interfaces\H5PExternalProviderInterface; @@ -33,26 +34,38 @@ protected function setUp(): void $this->testDisk = Storage::disk('testDisk'); $this->exportDisk = Storage::fake(); config(['h5p.storage.path' => $this->exportDisk->path("")]); + symlink($this->testDisk->path('files/libraries'), $this->exportDisk->path('libraries')); } - private function linkLibrariesFolder() + protected function tearDown(): void { - symlink($this->testDisk->path('files/libraries'), $this->exportDisk->path('libraries')); + parent::tearDown(); + + unlink($this->exportDisk->path('libraries')); } /** * @test * @throws Exception + * @dataProvider provider_noMultimedia */ - public function noMultimedia() + public function noMultimedia(bool $usePatchFolder) { $this->setupH5PAdapter([ 'alterLibrarySemantics' => null, 'getExternalProviders' => [], ]); - $this->linkLibrariesFolder(); $this->seed(TestH5PSeeder::class); + + if ($usePatchFolder) { + $lib = H5PLibrary::find(284); + $lib->minor_version = 14; + $lib->patch_version = 6; + $lib->patch_version_in_folder_name = true; + $lib->save(); + } + $params = '{"text":"

Fill in the missing words<\/p>\n","overallFeedback":[{"from":0,"to":100}],"showSolutions":"Show solution","tryAgain":"Retry","checkAnswer":"Check","notFilledOut":"Please fill in all blanks to view solution","answerIsCorrect":"\':ans\' is correct","answerIsWrong":"\':ans\' is wrong","answeredCorrectly":"Answered correctly","answeredIncorrectly":"Answered incorrectly","solutionLabel":"Correct answer:","inputLabel":"Blank input @num of @total","inputHasTipLabel":"Tip available","tipLabel":"Tip","behaviour":{"enableRetry":true,"enableSolutionsButton":true,"enableCheckButton":true,"autoCheck":false,"caseSensitive":true,"showSolutionsRequiresInput":true,"separateLines":false,"disableImageZooming":false,"confirmCheckDialog":false,"confirmRetryDialog":false,"acceptSpellingErrors":false},"scoreBarLabel":"You got :num out of :total points","confirmCheck":{"header":"Finish ?","body":"Are you sure you wish to finish ?","cancelLabel":"Cancel","confirmLabel":"Finish"},"confirmRetry":{"header":"Retry ?","body":"Are you sure you wish to retry ?","cancelLabel":"Cancel","confirmLabel":"Confirm"},"questions":["

*Fishing* is a fun *activity*.<\/p>\n"]}'; $h5p = H5PContent::factory()->create([ 'parameters' => $params, @@ -74,6 +87,12 @@ public function noMultimedia() $zipArchive->close(); } + public function provider_noMultimedia(): \Generator + { + yield 'minorFolder' => [false]; + yield 'patchFolder' => [true]; + } + /** * @test * @throws Exception @@ -85,7 +104,6 @@ public function withLocalImage() 'getExternalProviders' => [], ]); - $this->linkLibrariesFolder(); $this->seed(TestH5PSeeder::class); $params = '{"media":{"params":{"contentName":"Image","file":{"path":"images\/file-5f6ca98160e6c.jpg","mime":"image\/jpeg","copyright":{"license":"U"},"width":196,"height":358}},"library":"H5P.Image 1.1","metadata":{"contentType":"Image","license":"U","title":"Untitled Image","authors":[],"changes":[],"extraTitle":"Untitled Image"},"subContentId":"ca86c100-d25c-4e19-ac6b-f50f843da292"},"text":"Fill in the missing words","overallFeedback":[{"from":0,"to":100}],"showSolutions":"Show solution","tryAgain":"Retry","checkAnswer":"Check","notFilledOut":"Please fill in all blanks to view solution","answerIsCorrect":"':ans' is correct","answerIsWrong":"':ans' is wrong","answeredCorrectly":"Answered correctly","answeredIncorrectly":"Answered incorrectly","solutionLabel":"Correct answer:","inputLabel":"Blank input @num of @total","inputHasTipLabel":"Tip available","tipLabel":"Tip","behaviour":{"enableRetry":true,"enableSolutionsButton":true,"enableCheckButton":true,"autoCheck":false,"caseSensitive":true,"showSolutionsRequiresInput":true,"separateLines":false,"disableImageZooming":false,"confirmCheckDialog":false,"confirmRetryDialog":false,"acceptSpellingErrors":false},"scoreBarLabel":"You got :num out of :total points","confirmCheck":{"header":"Finish ?","body":"Are you sure you wish to finish ?","cancelLabel":"Cancel","confirmLabel":"Finish"},"confirmRetry":{"header":"Retry ?","body":"Are you sure you wish to retry ?","cancelLabel":"Cancel","confirmLabel":"Confirm"},"questions":["

Not all *superheros* wear capes!<\/p>\n"]}'; $h5p = H5PContent::factory()->create([ @@ -125,7 +143,6 @@ public function withRemoteImage_noLocalConvert() $imageUrl = $this->faker->imageUrl(); - $this->linkLibrariesFolder(); $this->seed(TestH5PSeeder::class); $params = '{"media":{"params":{"contentName":"Image","file":{"path":"' . $imageUrl . '","mime":"image\/jpeg","copyright":{"license":"U"},"width":196,"height":358}},"library":"H5P.Image 1.1","metadata":{"contentType":"Image","license":"U","title":"Untitled Image","authors":[],"changes":[],"extraTitle":"Untitled Image"},"subContentId":"ca86c100-d25c-4e19-ac6b-f50f843da292"},"text":"Fill in the missing words","overallFeedback":[{"from":0,"to":100}],"showSolutions":"Show solution","tryAgain":"Retry","checkAnswer":"Check","notFilledOut":"Please fill in all blanks to view solution","answerIsCorrect":"':ans' is correct","answerIsWrong":"':ans' is wrong","answeredCorrectly":"Answered correctly","answeredIncorrectly":"Answered incorrectly","solutionLabel":"Correct answer:","inputLabel":"Blank input @num of @total","inputHasTipLabel":"Tip available","tipLabel":"Tip","behaviour":{"enableRetry":true,"enableSolutionsButton":true,"enableCheckButton":true,"autoCheck":false,"caseSensitive":true,"showSolutionsRequiresInput":true,"separateLines":false,"disableImageZooming":false,"confirmCheckDialog":false,"confirmRetryDialog":false,"acceptSpellingErrors":false},"scoreBarLabel":"You got :num out of :total points","confirmCheck":{"header":"Finish ?","body":"Are you sure you wish to finish ?","cancelLabel":"Cancel","confirmLabel":"Finish"},"confirmRetry":{"header":"Retry ?","body":"Are you sure you wish to retry ?","cancelLabel":"Cancel","confirmLabel":"Confirm"},"questions":["

Not all *superheros* wear capes!<\/p>\n"]}'; $h5p = H5PContent::factory()->create([ @@ -159,7 +176,7 @@ public function withRemoteImage_noLocalConvert() public function withRemoteImage_storeLocally() { $imageUrl = $this->faker->imageUrl(); - $this->linkLibrariesFolder(); + $this->seed(TestH5PSeeder::class); $params = '{"media":{"params":{"contentName":"Image","file":{"path":"' . $imageUrl . '","mime":"image\/jpeg","copyright":{"license":"U"},"width":196,"height":358}},"library":"H5P.Image 1.1","metadata":{"contentType":"Image","license":"U","title":"Untitled Image","authors":[],"changes":[],"extraTitle":"Untitled Image"},"subContentId":"ca86c100-d25c-4e19-ac6b-f50f843da292"},"text":"Fill in the missing words","overallFeedback":[{"from":0,"to":100}],"showSolutions":"Show solution","tryAgain":"Retry","checkAnswer":"Check","notFilledOut":"Please fill in all blanks to view solution","answerIsCorrect":"':ans' is correct","answerIsWrong":"':ans' is wrong","answeredCorrectly":"Answered correctly","answeredIncorrectly":"Answered incorrectly","solutionLabel":"Correct answer:","inputLabel":"Blank input @num of @total","inputHasTipLabel":"Tip available","tipLabel":"Tip","behaviour":{"enableRetry":true,"enableSolutionsButton":true,"enableCheckButton":true,"autoCheck":false,"caseSensitive":true,"showSolutionsRequiresInput":true,"separateLines":false,"disableImageZooming":false,"confirmCheckDialog":false,"confirmRetryDialog":false,"acceptSpellingErrors":false},"scoreBarLabel":"You got :num out of :total points","confirmCheck":{"header":"Finish ?","body":"Are you sure you wish to finish ?","cancelLabel":"Cancel","confirmLabel":"Finish"},"confirmRetry":{"header":"Retry ?","body":"Are you sure you wish to retry ?","cancelLabel":"Cancel","confirmLabel":"Confirm"},"questions":["

Not all *superheros* wear capes!<\/p>\n"]}'; $h5p = H5PContent::factory()->create([ @@ -219,7 +236,6 @@ public function withRemoteImage_storeLocally() */ public function withRemoteVideo_storeLocally() { - $this->linkLibrariesFolder(); $this->seed(TestH5PSeeder::class); $params = '{"interactiveVideo":{"video":{"startScreenOptions":{"title":"Interactive Video","hideStartTitle":false,"copyright":""},"textTracks":[{"label":"Subtitles","kind":"subtitles","srcLang":"en"}],"files":[{"path":"https://bc/12456","mime":"video/BrightCove","copyright":{"license":"U"}}]},"assets":{"interactions":[]},"summary":{"task":{"library":"H5P.Summary 1.8","params":{"intro":"Choose the correct statement.","summaries":[{"subContentId":"2363644d-ab92-4f93-8d11-0d6e916bd83d","tip":""}],"overallFeedback":[{"from":0,"to":100}],"solvedLabel":"Progress:","scoreLabel":"Wrong answers:","resultLabel":"Your result","labelCorrect":"Correct.","labelIncorrect":"Incorrect! Please try again.","labelCorrectAnswers":"Correct answers.","tipButtonLabel":"Show tip","scoreBarLabel":"You got :num out of :total points","progressText":"Progress :num of :total"},"subContentId":"211f821f-67f9-4717-a7a4-362c4d507545","metadata":{"contentType":"Summary","license":"U","title":"Untitled Summary"}},"displayAt":3}},"override":{"autoplay":false,"loop":false,"showBookmarksmenuOnLoad":false,"showRewind10":false,"preventSkipping":false,"deactivateSound":false},"l10n":{"interaction":"Interaction","play":"Play","pause":"Pause","mute":"Mute","unmute":"Unmute","quality":"Video Quality","captions":"Captions","close":"Close","fullscreen":"Fullscreen","exitFullscreen":"Exit Fullscreen","summary":"Summary","bookmarks":"Bookmarks","defaultAdaptivitySeekLabel":"Continue","continueWithVideo":"Continue with video","playbackRate":"Playback Rate","rewind10":"Rewind 10 Seconds","navDisabled":"Navigation is disabled","sndDisabled":"Sound is disabled","requiresCompletionWarning":"You need to answer all the questions correctly before continuing.","back":"Back","hours":"Hours","minutes":"Minutes","seconds":"Seconds","currentTime":"Current time:","totalTime":"Total time:","navigationHotkeyInstructions":"Use key k for starting and stopping video at any time","singleInteractionAnnouncement":"Interaction appeared:","multipleInteractionsAnnouncement":"Multiple interactions appeared.","videoPausedAnnouncement":"Video is paused","content":"Content"}}'; $h5p = H5PContent::factory()->create([ @@ -278,7 +294,7 @@ public function withRemoteVideo_storeLocally() public function withRemoteVideoAndImage_storeLocally() { $imageUrl = $this->faker->imageUrl(); - $this->linkLibrariesFolder(); + $this->seed(TestH5PSeeder::class); $params = '{"interactiveVideo":{"video":{"startScreenOptions":{"title":"Interactive Video","hideStartTitle":false,"copyright":""},"textTracks":[{"label":"Subtitles","kind":"subtitles","srcLang":"en"}],"files":[{"path":"https://bc/ref:12456","mime":"video/BrightCove","copyright":{"license":"U"}}]},"assets":{"interactions":[{"x":5.230125523012552,"y":13.011152416356877,"width":14.986376021798364,"height":10,"duration":{"from":0,"to":10},"libraryTitle":"Image","action":{"library":"H5P.Image 1.0","params":{"contentName":"Image","file":{"path":"' . $imageUrl . '","mime":"image/jpeg","copyright":{"license":"U"},"width":1100,"height":734},"alt":"test"},"subContentId":"b0bd1110-ada3-4afe-9be9-1a822ea9643d","metadata":{"contentType":"Image","license":"U"}},"visuals":{"backgroundColor":"rgba(0,0,0,0)","boxShadow":true},"pause":false,"displayType":"poster","buttonOnMobile":false,"goto":{"url":{"protocol":"http://"},"visualize":false,"type":""},"label":""}]},"summary":{"task":{"library":"H5P.Summary 1.8","params":{"intro":"Choose the correct statement.","summaries":[{"subContentId":"2363644d-ab92-4f93-8d11-0d6e916bd83d","tip":""}],"overallFeedback":[{"from":0,"to":100}],"solvedLabel":"Progress:","scoreLabel":"Wrong answers:","resultLabel":"Your result","labelCorrect":"Correct.","labelIncorrect":"Incorrect! Please try again.","labelCorrectAnswers":"Correct answers.","tipButtonLabel":"Show tip","scoreBarLabel":"You got :num out of :total points","progressText":"Progress :num of :total"},"subContentId":"211f821f-67f9-4717-a7a4-362c4d507545","metadata":{"contentType":"Summary","license":"U","title":"Untitled Summary"}},"displayAt":3}},"override":{"autoplay":false,"loop":false,"showBookmarksmenuOnLoad":false,"showRewind10":false,"preventSkipping":false,"deactivateSound":false},"l10n":{"interaction":"Interaction","play":"Play","pause":"Pause","mute":"Mute","unmute":"Unmute","quality":"Video Quality","captions":"Captions","close":"Close","fullscreen":"Fullscreen","exitFullscreen":"Exit Fullscreen","summary":"Summary","bookmarks":"Bookmarks","defaultAdaptivitySeekLabel":"Continue","continueWithVideo":"Continue with video","playbackRate":"Playback Rate","rewind10":"Rewind 10 Seconds","navDisabled":"Navigation is disabled","sndDisabled":"Sound is disabled","requiresCompletionWarning":"You need to answer all the questions correctly before continuing.","back":"Back","hours":"Hours","minutes":"Minutes","seconds":"Seconds","currentTime":"Current time:","totalTime":"Total time:","navigationHotkeyInstructions":"Use key k for starting and stopping video at any time","singleInteractionAnnouncement":"Interaction appeared:","multipleInteractionsAnnouncement":"Multiple interactions appeared.","videoPausedAnnouncement":"Video is paused","content":"Content"}}'; $h5p = H5PContent::factory()->create([ @@ -352,7 +368,6 @@ public function withRemoteVideoAndImage_storeLocally() */ public function withRemoteCaptions_storeLocally() { - $this->linkLibrariesFolder(); $this->seed(TestH5PSeeder::class); $params = '{"interactiveVideo":{"video":{"startScreenOptions":{"title":"Interactive Video","hideStartTitle":false,"copyright":""},"textTracks":[{"kind":"captions","label":"Nynorsk","srcLang":"nn","track":{"externalId":"87ada56a-5670-4b43-96e3-4aef54284197","path":"https:\/\/urltocaptions/text.vtt","mime":"text\/webvtt","copyright":{"license":"U"}}}],"files":[{"path":"https://bc/12456","mime":"video/BrightCove","copyright":{"license":"U"}}]},"assets":{"interactions":[]},"summary":{"task":{"library":"H5P.Summary 1.8","params":{"intro":"Choose the correct statement.","summaries":[{"subContentId":"2363644d-ab92-4f93-8d11-0d6e916bd83d","tip":""}],"overallFeedback":[{"from":0,"to":100}],"solvedLabel":"Progress:","scoreLabel":"Wrong answers:","resultLabel":"Your result","labelCorrect":"Correct.","labelIncorrect":"Incorrect! Please try again.","labelCorrectAnswers":"Correct answers.","tipButtonLabel":"Show tip","scoreBarLabel":"You got :num out of :total points","progressText":"Progress :num of :total"},"subContentId":"211f821f-67f9-4717-a7a4-362c4d507545","metadata":{"contentType":"Summary","license":"U","title":"Untitled Summary"}},"displayAt":3}},"override":{"autoplay":false,"loop":false,"showBookmarksmenuOnLoad":false,"showRewind10":false,"preventSkipping":false,"deactivateSound":false},"l10n":{"interaction":"Interaction","play":"Play","pause":"Pause","mute":"Mute","unmute":"Unmute","quality":"Video Quality","captions":"Captions","close":"Close","fullscreen":"Fullscreen","exitFullscreen":"Exit Fullscreen","summary":"Summary","bookmarks":"Bookmarks","defaultAdaptivitySeekLabel":"Continue","continueWithVideo":"Continue with video","playbackRate":"Playback Rate","rewind10":"Rewind 10 Seconds","navDisabled":"Navigation is disabled","sndDisabled":"Sound is disabled","requiresCompletionWarning":"You need to answer all the questions correctly before continuing.","back":"Back","hours":"Hours","minutes":"Minutes","seconds":"Seconds","currentTime":"Current time:","totalTime":"Total time:","navigationHotkeyInstructions":"Use key k for starting and stopping video at any time","singleInteractionAnnouncement":"Interaction appeared:","multipleInteractionsAnnouncement":"Multiple interactions appeared.","videoPausedAnnouncement":"Video is paused","content":"Content"}}'; $h5p = H5PContent::factory()->create([ diff --git a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php index 8a631e8f4e..b40e010a0c 100644 --- a/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php +++ b/sourcecode/apis/contentauthor/tests/Integration/Libraries/H5P/Storage/H5pCerpusStorageTest.php @@ -2,24 +2,23 @@ namespace Tests\Integration\Libraries\H5P\Storage; +use App\H5PLibrary; use App\Libraries\ContentAuthorStorage; +use App\Libraries\DataObjects\ContentStorageSettings; use App\Libraries\H5P\Storage\H5PCerpusStorage; use App\Libraries\H5P\Video\NullVideoAdapter; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; use Psr\Log\NullLogger; use Tests\TestCase; class H5pCerpusStorageTest extends TestCase { - protected function setUp(): void - { - $this->markTestIncomplete('Fix these later'); - } + use RefreshDatabase; public function test_correct_url_without_cdn_prefix() { - $disk = Storage::fake('fake'); - + $disk = Storage::fake('test'); $disk->put('test.txt', 'some content'); $cerpusStorage = new H5pCerpusStorage( @@ -28,27 +27,26 @@ public function test_correct_url_without_cdn_prefix() new NullVideoAdapter(), ); - $this->assertEquals("/test.txt", $cerpusStorage->getFileUrl('test.txt')); + $this->assertEquals("http://localhost/content/assets/test.txt", $cerpusStorage->getFileUrl('test.txt')); } public function test_correct_url_with_cdn_prefix() { - $disk = Storage::fake('fake'); - + $disk = Storage::fake('test'); $disk->put('test.txt', 'some content'); $cerpusStorage = new H5pCerpusStorage( - new ContentAuthorStorage(''), + new ContentAuthorStorage('https://not.localhost.test/prefix/'), new NullLogger(), new NullVideoAdapter(), ); - $this->assertEquals("http://test/aaa/test.txt", $cerpusStorage->getFileUrl('test.txt')); + $this->assertEquals("https://not.localhost.test/prefix/test.txt", $cerpusStorage->getFileUrl('test.txt')); } public function test_correct_url_when_file_not_found() { - $disk = Storage::fake('fake'); + Storage::fake('test'); $cerpusStorage = new H5pCerpusStorage( new ContentAuthorStorage(''), @@ -56,7 +54,47 @@ public function test_correct_url_when_file_not_found() new NullVideoAdapter(), ); - $this->assertEquals("", $cerpusStorage->getFileUrl('test.txt')); } + + /** @dataProvider provide_test_getUpdateScript */ + public function test_getUpgradeScript(array $libConfig): void + { + $disk = Storage::fake(); + + /** @var H5PLibrary $library */ + $library = H5PLibrary::factory()->create($libConfig); + $file = sprintf(ContentStorageSettings::UPGRADE_SCRIPT_PATH, $library->getFolderName()); + + $this->assertFalse($disk->exists($file)); + $disk->put($file, 'just testing'); + $this->assertTrue($disk->exists($file)); + + $cerpusStorage = new H5pCerpusStorage( + new ContentAuthorStorage(''), + new NullLogger(), + new NullVideoAdapter(), + ); + + $path = $cerpusStorage->getUpgradeScript($library->name, $library->major_version, $library->minor_version); + + $this->assertStringContainsString($file, $path); + } + + public function provide_test_getUpdateScript(): \Generator + { + yield 'withoutPatch' => [[ + 'name' => 'H5P.Blanks', + 'major_version' => 1, + 'minor_version' => 11, + ]]; + + yield 'withPatch' => [[ + 'name' => 'H5P.Blanks', + 'major_version' => 1, + 'minor_version' => 14, + 'patch_version' => 6, + 'patch_version_in_folder_name' => 1, + ]]; + } } diff --git a/sourcecode/apis/contentauthor/tests/Integration/Models/H5PLibraryTest.php b/sourcecode/apis/contentauthor/tests/Integration/Models/H5PLibraryTest.php new file mode 100644 index 0000000000..5484e64fef --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/Integration/Models/H5PLibraryTest.php @@ -0,0 +1,100 @@ +make([ + 'patch_version_in_folder_name' => $hasPatch, + ]); + + $this->assertSame($expected, $lib->getLibraryString($usePatch)); + } + + public function provider_getLibraryString(): \Generator + { + yield 0 => [null, false, 'H5P.Foobar 1.2']; + yield 1 => [null, true, 'H5P.Foobar 1.2.3']; + yield 2 => [true, false, 'H5P.Foobar 1.2.3']; + yield 3 => [true, true, 'H5P.Foobar 1.2.3']; + yield 4 => [false, false, 'H5P.Foobar 1.2']; + yield 5 => [false, true, 'H5P.Foobar 1.2']; + } + + /** + * @dataProvider provider_getFolderName + */ + public function test_getFolderName($usePatch, $hasPatch, $expected): void + { + /** @var H5PLibrary $lib */ + $lib = H5PLibrary::factory()->make([ + 'patch_version_in_folder_name' => $hasPatch, + ]); + + $this->assertSame($expected, $lib->getFolderName($usePatch)); + } + + public function provider_getFolderName(): \Generator + { + yield 0 => [null, false, 'H5P.Foobar-1.2']; + yield 1 => [null, true, 'H5P.Foobar-1.2.3']; + yield 2 => [true, false, 'H5P.Foobar-1.2.3']; + yield 3 => [true, true, 'H5P.Foobar-1.2.3']; + yield 4 => [false, false, 'H5P.Foobar-1.2']; + yield 5 => [false, true, 'H5P.Foobar-1.2']; + } + + /** @dataProvider provider_libraryToFolderName */ + public function test_libraryToFolderName($data, $usePatch, $expected): void + { + $this->assertSame($expected, H5PLibrary::libraryToFolderName($data, $usePatch)); + } + + public function provider_libraryToFolderName(): \Generator + { + yield 0 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => true], false, 'H5P.Foobar-2.1']; + yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => false], true, 'H5P.Foobar-2.1.4']; + yield 2 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => 0], null, 'H5P.Foobar-2.1']; + yield 3 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => 1], null, 'H5P.Foobar-2.1.4']; + yield 4 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4], true, 'H5P.Foobar-2.1.4']; + yield 5 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4], null, 'H5P.Foobar-2.1']; + yield 6 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersionInFolderName' => true], false, 'H5P.Foobar-2.1']; + } + + /** @dataProvider provider_libraryToFolderNameExceptions */ + public function test_libraryToFolderNameExceptions($data, $usePatch): void + { + $this->expectException(\InvalidArgumentException::class); + H5PLibrary::libraryToFolderName($data, $usePatch); + } + + public function provider_libraryToFolderNameExceptions(): \Generator + { + yield 0 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersionInFolderName' => true], true]; + yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersionInFolderName' => false], true]; + yield 2 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersionInFolderName' => true], null]; + } + + /** @dataProvider provider_libraryToString */ + public function test_libraryToString($data, $usePatch, $expected): void + { + $this->assertSame($expected, H5PLibrary::libraryToString($data, $usePatch)); + } + + public function provider_libraryToString(): \Generator + { + yield 0 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => true], null, 'H5P.Foobar 2.1.4']; + yield 1 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => false], null, 'H5P.Foobar 2.1']; + yield 2 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => true], false, 'H5P.Foobar 2.1']; + yield 3 => [['name' => 'H5P.Foobar', 'majorVersion' => 2, 'minorVersion' => 1, 'patchVersion' => 4, 'patchVersionInFolderName' => false], true, 'H5P.Foobar 2.1.4']; + } +} diff --git a/sourcecode/apis/contentauthor/tests/Unit/Http/Controllers/AdminH5PDetailsControllerTest.php b/sourcecode/apis/contentauthor/tests/Unit/Http/Controllers/AdminH5PDetailsControllerTest.php index 7f953b9f00..855444842e 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/Http/Controllers/AdminH5PDetailsControllerTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/Http/Controllers/AdminH5PDetailsControllerTest.php @@ -10,6 +10,7 @@ use App\Libraries\ContentAuthorStorage; use App\Libraries\H5P\Framework; use Cerpus\VersionClient\VersionData; +use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Http\Request; @@ -81,7 +82,14 @@ public function test_checkLibrary(): void $this->instance(ContentAuthorStorage::class, $storage); $storage ->expects($this->once()) - ->method('copyFolder'); + ->method('copyFolder') + ->with( + $this->isInstanceOf(FilesystemAdapter::class), + $this->isInstanceOf(FilesystemAdapter::class), + $this->equalTo('libraries/H5P.Foobar-1.2'), + $this->equalTo('libraries/H5P.Foobar-1.2'), + $this->equalTo([]), + ); $framework = $this->createMock(Framework::class); $this->instance(Framework::class, $framework); @@ -97,6 +105,7 @@ public function test_checkLibrary(): void $validator ->expects($this->once()) ->method('getLibraryData') + ->with($this->equalTo('H5P.Foobar-1.2'), $this->isNull(), $this->isNull()) ->willReturn([ 'editorDependencies' => [ [ diff --git a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php index 6e65d103ee..29deee12d3 100644 --- a/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php +++ b/sourcecode/apis/contentauthor/tests/Unit/Libraries/H5P/FrameworkTest.php @@ -31,6 +31,8 @@ final class FrameworkTest extends TestCase protected function setUp(): void { + parent::setUp(); + $this->history = new ArrayObject(); $this->mockedResponses = new MockHandler(); @@ -87,6 +89,30 @@ public function testFetchExternalDataWithData(): void ); } + public function testFetchExternalDataWithFullData(): void + { + $this->mockedResponses->append(new Response(200, [], 'Some body')); + + $response = $this->framework->fetchExternalData( + 'http://www.example.com', + [ + 'foo' => 'bar', + ], + fullData: true, + ); + + $this->assertSame( + 'foo=bar', + $this->history[0]['request']->getBody()->getContents(), + ); + $this->assertIsArray($response); + $this->assertArrayHasKey('status', $response); + $this->assertArrayHasKey('headers', $response); + $this->assertArrayHasKey('data', $response); + $this->assertSame(200, $response['status']); + $this->assertSame('Some body', $response['data']); + } + public function testFetchExternalDataWithGuzzleError(): void { $this->mockedResponses->append(new TransferException()); diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/css/blanks.css b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/css/blanks.css new file mode 100644 index 0000000000..b5e99c0986 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/css/blanks.css @@ -0,0 +1,113 @@ +.h5p-blanks { + position: relative; +} +.h5p-blanks .h5p-input-wrapper { + display: inline-block; + position: relative; +} +.h5p-blanks .h5p-text-input { + font-family: H5PDroidSans, sans-serif; + font-size: 1em; + border-radius: 0.25em; + border: 1px solid #a0a0a0; + padding: 0.1875em 1em 0.1875em 0.5em; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 6em; +} +.h5p-blanks .h5p-text-input:focus { + outline: none; + box-shadow: 0 0 0.5em 0 #6391CA; + border-color: #6391CA; +} +.h5p-blanks .h5p-text-input:disabled { + opacity: 1; +} +.h5p-blanks .h5p-text-input.h5p-not-filled-out { + background: #fff0f0; +} +.h5p-blanks .h5p-separate-lines .h5p-input-wrapper { + display: block; +} +.h5p-blanks .h5p-separate-lines .h5p-text-input { + width: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; +} + +/* Correctly answered input */ +.h5p-blanks .h5p-correct .h5p-text-input { + background: #9dd8bb; + border: 1px solid #9dd8bb; + color: #255c41; +} +/* Showing solution */ +.h5p-blanks .h5p-correct-answer { + color: #255c41; + font-weight: bold; + border: 1px #255c41 dashed; + background-color: #d4f6e6; + padding: 0.15em; + border-radius: 0.25em; + margin-left: .5em; +} +.h5p-blanks .h5p-correct:after { + position: absolute; + right: 0.5em; + top: 0; + text-decoration: none; + content: "\f00c"; + font-family: 'H5PFontAwesome4'; + color: #255c41; +} + +/* Wrongly answered input */ +.h5p-blanks .h5p-wrong .h5p-text-input { + background-color: #f7d0d0; + border: 1px solid #f7d0d0; + color: #b71c1c; + text-decoration: line-through; +} +.h5p-blanks .h5p-wrong:after { + position: absolute; + right: 0.5em; + top: 0; + font-family: 'H5PFontAwesome4'; + text-decoration: none; + content: "\f00d"; + color: #b71c1c; +} + +/* Actual text paragraphs */ +.h5p-blanks .h5p-question-content p { + line-height: 1.75em; + margin: 0 0 1em; +} + +/* Header and footer blocks (title + evaluation, buttons) */ +.h5p-blanks .joubel-tip-container { + position: absolute; + right: 0.4em; + font-size: 1em; +} +.h5p-blanks .joubel-tip-container .joubel-icon-tip-normal { + line-height: 1em; +} +.h5p-blanks .has-tip .h5p-text-input { + padding-right: 2.25em; +} +.h5p-blanks .has-tip.h5p-correct:after, +.h5p-blanks .has-tip.h5p-wrong:after { + right: 2.25em; +} +.h5p-blanks .has-tip.h5p-correct .h5p-text-input, +.h5p-blanks .has-tip.h5p-wrong .h5p-text-input { + padding-right: 3.5em; +} +.h5p-blanks .hidden-but-read { + position: absolute; + height: 0; + width: 0; + overflow: hidden; +} diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/icon.svg b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/icon.svg new file mode 100644 index 0000000000..ef1db071e0 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/icon.svg @@ -0,0 +1,68 @@ + + + + + fill in the blanks + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/blanks.js b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/blanks.js new file mode 100644 index 0000000000..30194deb3b --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/blanks.js @@ -0,0 +1,1004 @@ +/*global H5P*/ +H5P.Blanks = (function ($, Question) { + /** + * @constant + * @default + */ + var STATE_ONGOING = 'ongoing'; + var STATE_CHECKING = 'checking'; + var STATE_SHOWING_SOLUTION = 'showing-solution'; + var STATE_FINISHED = 'finished'; + + const XAPI_ALTERNATIVE_EXTENSION = 'https://h5p.org/x-api/alternatives'; + const XAPI_CASE_SENSITIVITY = 'https://h5p.org/x-api/case-sensitivity'; + const XAPI_REPORTING_VERSION_EXTENSION = 'https://h5p.org/x-api/h5p-reporting-version'; + + /** + * @typedef {Object} Params + * Parameters/configuration object for Blanks + * + * @property {Object} Params.behaviour + * @property {string} Params.behaviour.confirmRetryDialog + * @property {string} Params.behaviour.confirmCheckDialog + * + * @property {Object} Params.confirmRetry + * @property {string} Params.confirmRetry.header + * @property {string} Params.confirmRetry.body + * @property {string} Params.confirmRetry.cancelLabel + * @property {string} Params.confirmRetry.confirmLabel + * + * @property {Object} Params.confirmCheck + * @property {string} Params.confirmCheck.header + * @property {string} Params.confirmCheck.body + * @property {string} Params.confirmCheck.cancelLabel + * @property {string} Params.confirmCheck.confirmLabel + */ + + /** + * Initialize module. + * + * @class H5P.Blanks + * @extends H5P.Question + * @param {Params} params + * @param {number} id Content identification + * @param {Object} contentData Task specific content data + */ + function Blanks(params, id, contentData) { + var self = this; + + // Inheritance + Question.call(self, 'blanks'); + + // IDs + this.contentId = id; + this.contentData = contentData; + + this.params = $.extend(true, {}, { + text: "Fill in", + questions: [ + "

Oslo is the capital of *Norway*.

" + ], + overallFeedback: [], + userAnswers: [], // TODO This isn't in semantics? + showSolutions: "Show solution", + tryAgain: "Try again", + checkAnswer: "Check", + changeAnswer: "Change answer", + notFilledOut: "Please fill in all blanks to view solution", + answerIsCorrect: "':ans' is correct", + answerIsWrong: "':ans' is wrong", + answeredCorrectly: "Answered correctly", + answeredIncorrectly: "Answered incorrectly", + solutionLabel: "Correct answer:", + inputLabel: "Blank input @num of @total", + inputHasTipLabel: "Tip available", + tipLabel: "Tip", + scoreBarLabel: 'You got :num out of :total points', + behaviour: { + enableRetry: true, + enableSolutionsButton: true, + enableCheckButton: true, + caseSensitive: true, + showSolutionsRequiresInput: true, + autoCheck: false, + separateLines: false + }, + a11yCheck: 'Check the answers. The responses will be marked as correct, incorrect, or unanswered.', + a11yShowSolution: 'Show the solution. The task will be marked with its correct solution.', + a11yRetry: 'Retry the task. Reset all responses and start the task over again.', + a11yHeader: 'Checking mode', + submitAnswer: 'Submit', + }, params); + + // Delete empty questions + for (var i = this.params.questions.length - 1; i >= 0; i--) { + if (!this.params.questions[i]) { + this.params.questions.splice(i, 1); + } + } + + // Previous state + this.contentData = contentData; + if (this.contentData !== undefined && this.contentData.previousState !== undefined) { + this.previousState = this.contentData.previousState; + } + + // Clozes + this.clozes = []; + + // Keep track tabbing forward or backwards + this.shiftPressed = false; + + H5P.$body.keydown(function (event) { + if (event.keyCode === 16) { + self.shiftPressed = true; + } + }).keyup(function (event) { + if (event.keyCode === 16) { + self.shiftPressed = false; + } + }); + + // Using instructions as label for our text groups + this.labelId = 'h5p-blanks-instructions-' + Blanks.idCounter; + this.content = self.createQuestions(); + + // Check for task media + var media = self.params.media; + if (media && media.type && media.type.library) { + media = media.type; + var type = media.library.split(' ')[0]; + if (type === 'H5P.Image') { + if (media.params.file) { + // Register task image + self.setImage(media.params.file.path, { + disableImageZooming: self.params.media.disableImageZooming || false, + alt: media.params.alt, + title: media.params.title + }); + } + } + else if (type === 'H5P.Video') { + if (media.params.sources) { + // Register task video + self.setVideo(media); + } + } + else if (type === 'H5P.Audio') { + if (media.params.files) { + // Register task audio + self.setAudio(media); + } + } + } + + // Register task introduction text + self.setIntroduction('
' + self.params.text + '
'); + + // Register task content area + self.setContent(self.content, { + 'class': self.params.behaviour.separateLines ? 'h5p-separate-lines' : '' + }); + + // ... and buttons + self.registerButtons(); + + // Restore previous state + self.setH5PUserState(); + } + + // Inheritance + Blanks.prototype = Object.create(Question.prototype); + Blanks.prototype.constructor = Blanks; + + /** + * Create all the buttons for the task + */ + Blanks.prototype.registerButtons = function () { + var self = this; + + var $content = $('[data-content-id="' + self.contentId + '"].h5p-content'); + var $containerParents = $content.parents('.h5p-container'); + + // select find container to attach dialogs to + var $container; + if ($containerParents.length !== 0) { + // use parent highest up if any + $container = $containerParents.last(); + } + else if ($content.length !== 0) { + $container = $content; + } + else { + $container = $(document.body); + } + + if (!self.params.behaviour.autoCheck && this.params.behaviour.enableCheckButton) { + // Check answer button + self.addButton('check-answer', self.params.checkAnswer, function () { + // Move focus to top of content + self.a11yHeader.innerHTML = self.params.a11yHeader; + self.a11yHeader.focus(); + + self.toggleButtonVisibility(STATE_CHECKING); + self.markResults(); + self.showEvaluation(); + self.triggerAnswered(); + }, true, { + 'aria-label': self.params.a11yCheck, + }, { + confirmationDialog: { + enable: self.params.behaviour.confirmCheckDialog, + l10n: self.params.confirmCheck, + instance: self, + $parentElement: $container + }, + textIfSubmitting: self.params.submitAnswer, + contentData: self.contentData, + }); + } + + // Show solution button + self.addButton('show-solution', self.params.showSolutions, function () { + self.showCorrectAnswers(false); + }, self.params.behaviour.enableSolutionsButton, { + 'aria-label': self.params.a11yShowSolution, + }); + + // Try again button + if (self.params.behaviour.enableRetry === true) { + self.addButton('try-again', self.params.tryAgain, function () { + self.a11yHeader.innerHTML = ''; + self.resetTask(); + self.$questions.filter(':first').find('input:first').focus(); + }, true, { + 'aria-label': self.params.a11yRetry, + }, { + confirmationDialog: { + enable: self.params.behaviour.confirmRetryDialog, + l10n: self.params.confirmRetry, + instance: self, + $parentElement: $container + } + }); + } + self.toggleButtonVisibility(STATE_ONGOING); + }; + + /** + * Find blanks in a string and run a handler on those blanks + * + * @param {string} question + * Question text containing blanks enclosed in asterisks. + * @param {function} handler + * Replaces the blanks text with an input field. + * @returns {string} + * The question with blanks replaced by the given handler. + */ + Blanks.prototype.handleBlanks = function (question, handler) { + // Go through the text and run handler on all asterisk + var clozeEnd, clozeStart = question.indexOf('*'); + var self = this; + while (clozeStart !== -1 && clozeEnd !== -1) { + clozeStart++; + clozeEnd = question.indexOf('*', clozeStart); + if (clozeEnd === -1) { + continue; // No end + } + var clozeContent = question.substring(clozeStart, clozeEnd); + var replacer = ''; + if (clozeContent.length) { + replacer = handler(self.parseSolution(clozeContent)); + clozeEnd++; + } + else { + clozeStart += 1; + } + question = question.slice(0, clozeStart - 1) + replacer + question.slice(clozeEnd); + clozeEnd -= clozeEnd - clozeStart - replacer.length; + + // Find the next cloze + clozeStart = question.indexOf('*', clozeEnd); + } + return question; + }; + + /** + * Create questitons html for DOM + */ + Blanks.prototype.createQuestions = function () { + var self = this; + + var html = ''; + var clozeNumber = 0; + for (var i = 0; i < self.params.questions.length; i++) { + var question = self.params.questions[i]; + + // Go through the question text and replace all the asterisks with input fields + question = self.handleBlanks(question, function (solution) { + // Create new cloze + clozeNumber += 1; + var defaultUserAnswer = (self.params.userAnswers.length > self.clozes.length ? self.params.userAnswers[self.clozes.length] : null); + var cloze = new Blanks.Cloze(solution, self.params.behaviour, defaultUserAnswer, { + answeredCorrectly: self.params.answeredCorrectly, + answeredIncorrectly: self.params.answeredIncorrectly, + solutionLabel: self.params.solutionLabel, + inputLabel: self.params.inputLabel, + inputHasTipLabel: self.params.inputHasTipLabel, + tipLabel: self.params.tipLabel + }); + + self.clozes.push(cloze); + return cloze; + }); + + html += '
' + question + '
'; + } + + self.hasClozes = clozeNumber > 0; + this.$questions = $(html); + + self.a11yHeader = document.createElement('div'); + self.a11yHeader.classList.add('hidden-but-read'); + self.a11yHeader.tabIndex = -1; + self.$questions[0].insertBefore(self.a11yHeader, this.$questions[0].childNodes[0] || null); + + // Set input fields. + this.$questions.find('input').each(function (i) { + + /** + * Observe resizing of input field, so that we can resize + * the H5P to fit all content when the input field grows in size + */ + let resizeTimer; + new ResizeObserver(function () { + // To avoid triggering resize too often, we wait a second after the last + // resize event has been received + clearTimeout(resizeTimer); + resizeTimer = setTimeout(function () { + self.trigger('resize'); + }, 1000); + }).observe(this); + + var afterCheck; + if (self.params.behaviour.autoCheck) { + afterCheck = function () { + var answer = $("
").text(this.getUserAnswer()).html(); + self.read((this.correct() ? self.params.answerIsCorrect : self.params.answerIsWrong).replace(':ans', answer)); + if (self.done || self.allBlanksFilledOut()) { + // All answers has been given. Show solutions button. + self.toggleButtonVisibility(STATE_CHECKING); + self.showEvaluation(); + self.triggerAnswered(); + self.done = true; + } + }; + } + self.clozes[i].setInput($(this), afterCheck, function () { + self.toggleButtonVisibility(STATE_ONGOING); + if (!self.params.behaviour.autoCheck) { + self.hideEvaluation(); + } + }, i, self.clozes.length); + }).keydown(function (event) { + var $this = $(this); + + // Adjust width of text input field to match value + self.autoGrowTextField($this); + + var $inputs, isLastInput; + var enterPressed = (event.keyCode === 13); + var tabPressedAutoCheck = (event.keyCode === 9 && self.params.behaviour.autoCheck); + + if (enterPressed || tabPressedAutoCheck) { + // Figure out which inputs are left to answer + $inputs = self.$questions.find('.h5p-input-wrapper:not(.h5p-correct) .h5p-text-input'); + + // Figure out if this is the last input + isLastInput = $this.is($inputs[$inputs.length - 1]); + } + + if ((tabPressedAutoCheck && isLastInput && !self.shiftPressed) || + (enterPressed && isLastInput)) { + // Focus first button on next tick + setTimeout(function () { + self.focusButton(); + }, 10); + } + + if (enterPressed) { + if (isLastInput) { + // Check answers + $this.trigger('blur'); + } + else { + // Find next input to focus + $inputs.eq($inputs.index($this) + 1).focus(); + } + + return false; // Prevent form submission on enter key + } + }).on('change', function () { + self.answered = true; + self.triggerXAPI('interacted'); + }); + + self.on('resize', function () { + self.resetGrowTextField(); + }); + + return this.$questions; + }; + + /** + * + */ + Blanks.prototype.autoGrowTextField = function ($input) { + // Do not set text field size when separate lines is enabled + if (this.params.behaviour.separateLines) { + return; + } + + var self = this; + var fontSize = parseInt($input.css('font-size'), 10); + var minEm = 3; + var minPx = fontSize * minEm; + var rightPadEm = 3.25; + var rightPadPx = fontSize * rightPadEm; + var static_min_pad = 0.5 * fontSize; + + setTimeout(function () { + var tmp = $('
', { + 'text': $input.val() + }); + tmp.css({ + 'position': 'absolute', + 'white-space': 'nowrap', + 'font-size': $input.css('font-size'), + 'font-family': $input.css('font-family'), + 'padding': $input.css('padding'), + 'width': 'initial' + }); + $input.parent().append(tmp); + var width = tmp.width(); + var parentWidth = self.$questions.width(); + tmp.remove(); + if (width <= minPx) { + // Apply min width + $input.width(minPx + static_min_pad); + } + else if (width + rightPadPx >= parentWidth) { + // Apply max width of parent + $input.width(parentWidth - rightPadPx); + } + else { + // Apply width that wraps input + $input.width(width + static_min_pad); + } + }, 1); + }; + + /** + * Resize all text field growth to current size. + */ + Blanks.prototype.resetGrowTextField = function () { + var self = this; + + this.$questions.find('input').each(function () { + self.autoGrowTextField($(this)); + }); + }; + + /** + * Toggle buttons dependent of state. + * + * Using CSS-rules to conditionally show/hide using the data-attribute [data-state] + */ + Blanks.prototype.toggleButtonVisibility = function (state) { + // The show solutions button is hidden if all answers are correct + var allCorrect = (this.getScore() === this.getMaxScore()); + if (this.params.behaviour.autoCheck && allCorrect) { + // We are viewing the solutions + state = STATE_FINISHED; + } + + if (this.params.behaviour.enableSolutionsButton) { + if (state === STATE_CHECKING && !allCorrect) { + this.showButton('show-solution'); + } + else { + this.hideButton('show-solution'); + } + } + + if (this.params.behaviour.enableRetry) { + if ((state === STATE_CHECKING && !allCorrect) || state === STATE_SHOWING_SOLUTION) { + this.showButton('try-again'); + } + else { + this.hideButton('try-again'); + } + } + + if (state === STATE_ONGOING) { + this.showButton('check-answer'); + } + else { + this.hideButton('check-answer'); + } + + this.trigger('resize'); + }; + + /** + * Check if solution is allowed. Warn user if not + */ + Blanks.prototype.allowSolution = function () { + if (this.params.behaviour.showSolutionsRequiresInput === true) { + if (!this.allBlanksFilledOut()) { + this.updateFeedbackContent(this.params.notFilledOut); + this.read(this.params.notFilledOut); + return false; + } + } + return true; + }; + + /** + * Check if all blanks are filled out + * + * @method allBlanksFilledOut + * @return {boolean} Returns true if all blanks are filled out. + */ + Blanks.prototype.allBlanksFilledOut = function () { + return !this.clozes.some(function (cloze) { + return !cloze.filledOut(); + }); + }; + + /** + * Mark which answers are correct and which are wrong and disable fields if retry is off. + */ + Blanks.prototype.markResults = function () { + var self = this; + for (var i = 0; i < self.clozes.length; i++) { + self.clozes[i].checkAnswer(); + if (!self.params.behaviour.enableRetry) { + self.clozes[i].disableInput(); + } + } + this.trigger('resize'); + }; + + /** + * Removed marked results + */ + Blanks.prototype.removeMarkedResults = function () { + this.$questions.find('.h5p-input-wrapper').removeClass('h5p-correct h5p-wrong'); + this.$questions.find('.h5p-input-wrapper > input').attr('disabled', false); + this.trigger('resize'); + }; + + + /** + * Displays the correct answers + * @param {boolean} [alwaysShowSolution] + * Will always show solution if true + */ + Blanks.prototype.showCorrectAnswers = function (alwaysShowSolution) { + if (!alwaysShowSolution && !this.allowSolution()) { + return; + } + + this.toggleButtonVisibility(STATE_SHOWING_SOLUTION); + this.hideSolutions(); + + for (var i = 0; i < this.clozes.length; i++) { + this.clozes[i].showSolution(); + } + this.trigger('resize'); + }; + + /** + * Toggle input allowed for all input fields + * + * @method function + * @param {boolean} enabled True if fields should allow input, otherwise false + */ + Blanks.prototype.toggleAllInputs = function (enabled) { + for (var i = 0; i < this.clozes.length; i++) { + this.clozes[i].toggleInput(enabled); + } + }; + + /** + * Display the correct solution for the input boxes. + * + * This is invoked from CP and QS - be carefull! + */ + Blanks.prototype.showSolutions = function () { + this.params.behaviour.enableSolutionsButton = true; + this.toggleButtonVisibility(STATE_FINISHED); + this.markResults(); + this.showEvaluation(); + this.showCorrectAnswers(true); + this.toggleAllInputs(false); + //Hides all buttons in "show solution" mode. + this.hideButtons(); + }; + + /** + * Resets the complete task. + * Used in contracts. + * @public + */ + Blanks.prototype.resetTask = function () { + this.answered = false; + this.hideEvaluation(); + this.hideSolutions(); + this.clearAnswers(); + this.removeMarkedResults(); + this.toggleButtonVisibility(STATE_ONGOING); + this.resetGrowTextField(); + this.toggleAllInputs(true); + this.done = false; + }; + + /** + * Hides all buttons. + * @public + */ + Blanks.prototype.hideButtons = function () { + this.toggleButtonVisibility(STATE_FINISHED); + }; + + /** + * Trigger xAPI answered event + */ + Blanks.prototype.triggerAnswered = function () { + this.answered = true; + var xAPIEvent = this.createXAPIEventTemplate('answered'); + this.addQuestionToXAPI(xAPIEvent); + this.addResponseToXAPI(xAPIEvent); + this.trigger(xAPIEvent); + }; + + /** + * Get xAPI data. + * Contract used by report rendering engine. + * + * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6} + */ + Blanks.prototype.getXAPIData = function () { + var xAPIEvent = this.createXAPIEventTemplate('answered'); + this.addQuestionToXAPI(xAPIEvent); + this.addResponseToXAPI(xAPIEvent); + return { + statement: xAPIEvent.data.statement + }; + }; + + /** + * Generate xAPI object definition used in xAPI statements. + * @return {Object} + */ + Blanks.prototype.getxAPIDefinition = function () { + var definition = {}; + definition.description = { + 'en-US': this.params.text + }; + definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction'; + definition.interactionType = 'fill-in'; + + const clozeSolutions = []; + let crp = ''; + // xAPI forces us to create solution patterns for all possible solution combinations + for (var i = 0; i < this.params.questions.length; i++) { + var question = this.handleBlanks(this.params.questions[i], function (solution) { + // Collect all solution combinations for the H5P Alternative extension + clozeSolutions.push(solution.solutions); + + // Create a basic response pattern out of the first alternative for each blanks field + crp += (!crp ? '' : '[,]') + solution.solutions[0]; + + // We replace the solutions in the question with a "blank" + return '__________'; + }); + definition.description['en-US'] += question; + } + + // Set the basic response pattern (not supporting multiple alternatives for blanks) + definition.correctResponsesPattern = [ + '{case_matters=' + this.params.behaviour.caseSensitive + '}' + crp, + ]; + + // Add the H5P Alternative extension which provides all the combinations of different answers + // Reporting software will need to support this extension for alternatives to work. + definition.extensions = definition.extensions || {}; + definition.extensions[XAPI_CASE_SENSITIVITY] = this.params.behaviour.caseSensitive; + definition.extensions[XAPI_ALTERNATIVE_EXTENSION] = clozeSolutions; + + return definition; + }; + + /** + * Add the question itselt to the definition part of an xAPIEvent + */ + Blanks.prototype.addQuestionToXAPI = function (xAPIEvent) { + var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']); + $.extend(true, definition, this.getxAPIDefinition()); + + // Set reporting module version if alternative extension is used + if (this.hasAlternatives) { + const context = xAPIEvent.getVerifiedStatementValue(['context']); + context.extensions = context.extensions || {}; + context.extensions[XAPI_REPORTING_VERSION_EXTENSION] = '1.1.0'; + } + }; + + /** + * Parse the solution text (text between the asterisks) + * + * @param {string} solutionText + * @returns {object} with the following properties + * - tip: the tip text for this solution, undefined if no tip + * - solutions: array of solution words + */ + Blanks.prototype.parseSolution = function (solutionText) { + var tip, solution; + + var tipStart = solutionText.indexOf(':'); + if (tipStart !== -1) { + // Found tip, now extract + tip = solutionText.slice(tipStart + 1); + solution = solutionText.slice(0, tipStart); + } + else { + solution = solutionText; + } + + // Split up alternatives + var solutions = solution.split('/'); + this.hasAlternatives = this.hasAlternatives || solutions.length > 1; + + // Trim solutions + for (var i = 0; i < solutions.length; i++) { + solutions[i] = H5P.trim(solutions[i]); + + //decodes html entities + var elem = document.createElement('textarea'); + elem.innerHTML = solutions[i]; + solutions[i] = elem.value; + } + + return { + tip: tip, + solutions: solutions + }; + }; + + /** + * Add the response part to an xAPI event + * + * @param {H5P.XAPIEvent} xAPIEvent + * The xAPI event we will add a response to + */ + Blanks.prototype.addResponseToXAPI = function (xAPIEvent) { + xAPIEvent.setScoredResult(this.getScore(), this.getMaxScore(), this); + xAPIEvent.data.statement.result.response = this.getxAPIResponse(); + }; + + /** + * Generate xAPI user response, used in xAPI statements. + * @return {string} User answers separated by the "[,]" pattern + */ + Blanks.prototype.getxAPIResponse = function () { + var usersAnswers = this.getCurrentState(); + return usersAnswers.join('[,]'); + }; + + /** + * Show evaluation widget, i.e: 'You got x of y blanks correct' + */ + Blanks.prototype.showEvaluation = function () { + var maxScore = this.getMaxScore(); + var score = this.getScore(); + var scoreText = H5P.Question.determineOverallFeedback(this.params.overallFeedback, score / maxScore).replace('@score', score).replace('@total', maxScore); + + this.setFeedback(scoreText, score, maxScore, this.params.scoreBarLabel); + + if (score === maxScore) { + this.toggleButtonVisibility(STATE_FINISHED); + } + }; + + /** + * Hide the evaluation widget + */ + Blanks.prototype.hideEvaluation = function () { + // Clear evaluation section. + this.removeFeedback(); + }; + + /** + * Hide solutions. (/try again) + */ + Blanks.prototype.hideSolutions = function () { + // Clean solution from quiz + this.$questions.find('.h5p-correct-answer').remove(); + }; + + /** + * Get maximum number of correct answers. + * + * @returns {Number} Max points + */ + Blanks.prototype.getMaxScore = function () { + var self = this; + return self.clozes.length; + }; + + /** + * Count the number of correct answers. + * + * @returns {Number} Points + */ + Blanks.prototype.getScore = function () { + var self = this; + var correct = 0; + for (var i = 0; i < self.clozes.length; i++) { + if (self.clozes[i].correct()) { + correct++; + } + self.params.userAnswers[i] = self.clozes[i].getUserAnswer(); + } + + return correct; + }; + + Blanks.prototype.getTitle = function () { + return H5P.createTitle((this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title : 'Fill In'); + }; + + /** + * Clear the user's answers + */ + Blanks.prototype.clearAnswers = function () { + this.clozes.forEach(function (cloze) { + cloze.setUserInput(''); + cloze.resetAriaLabel(); + }); + }; + + /** + * Checks if all has been answered. + * + * @returns {Boolean} + */ + Blanks.prototype.getAnswerGiven = function () { + return this.answered || !this.hasClozes; + }; + + /** + * Helps set focus the given input field. + * @param {jQuery} $input + */ + Blanks.setFocus = function ($input) { + setTimeout(function () { + $input.focus(); + }, 1); + }; + + /** + * Returns an object containing content of each cloze + * + * @returns {object} object containing content for each cloze + */ + Blanks.prototype.getCurrentState = function () { + var clozesContent = []; + + // Get user input for every cloze + this.clozes.forEach(function (cloze) { + clozesContent.push(cloze.getUserAnswer()); + }); + return clozesContent; + }; + + /** + * Sets answers to current user state + */ + Blanks.prototype.setH5PUserState = function () { + var self = this; + var isValidState = (this.previousState !== undefined && + this.previousState.length && + this.previousState.length === this.clozes.length); + + // Check that stored user state is valid + if (!isValidState) { + return; + } + + // Set input from user state + var hasAllClozesFilled = true; + this.previousState.forEach(function (clozeContent, ccIndex) { + + // Register that an answer has been given + if (clozeContent.length) { + self.answered = true; + } + + var cloze = self.clozes[ccIndex]; + cloze.setUserInput(clozeContent); + + // Handle instant feedback + if (self.params.behaviour.autoCheck) { + if (cloze.filledOut()) { + cloze.checkAnswer(); + } + else { + hasAllClozesFilled = false; + } + } + }); + + if (self.params.behaviour.autoCheck && hasAllClozesFilled) { + self.showEvaluation(); + self.toggleButtonVisibility(STATE_CHECKING); + } + }; + + /** + * Disables any active input. Useful for freezing the task and dis-allowing + * modification of wrong answers. + */ + Blanks.prototype.disableInput = function () { + this.$questions.find('input').attr('disabled', true); + }; + + Blanks.idCounter = 0; + + return Blanks; +})(H5P.jQuery, H5P.Question); + +/** + * Static utility method for parsing H5P.Blanks qestion into a format useful + * for creating reports. + * + * Example question: 'H5P content may be edited using a *browser/web-browser:something you use every day*.' + * + * Produces the following result: + * [ + * { + * type: 'text', + * content: 'H5P content may be edited using a ' + * }, + * { + * type: 'answer', + * correct: ['browser', 'web-browser'] + * }, + * { + * type: 'text', + * content: '.' + * } + * ] + * + * @param {string} question + */ +H5P.Blanks.parseText = function (question) { + var blank = new H5P.Blanks({ question: question }); + + /** + * Parses a text into an array where words starting and ending + * with an asterisk are separated from other text. + * e.g ["this", "*is*", " an ", "*example*"] + * + * @param {string} text + * + * @return {string[]} + */ + function tokenizeQuestionText(text) { + return text.split(/(\*.*?\*)/).filter(function (str) { + return str.length > 0; } + ); + } + + function startsAndEndsWithAnAsterisk(str) { + return str.substr(0,1) === '*' && str.substr(-1) === '*'; + } + + function replaceHtmlTags(str, value) { + return str.replace(/<[^>]*>/g, value); + } + + return tokenizeQuestionText(replaceHtmlTags(question, '')).map(function (part) { + return startsAndEndsWithAnAsterisk(part) ? + ({ + type: 'answer', + correct: blank.parseSolution(part.slice(1, -1)).solutions + }) : + ({ + type: 'text', + content: part + }); + }); +}; diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/cloze.js b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/cloze.js new file mode 100644 index 0000000000..8a60a7b430 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/js/cloze.js @@ -0,0 +1,238 @@ +(function ($, Blanks) { + + /** + * Simple private class for keeping track of clozes. + * + * @class H5P.Blanks.Cloze + * @param {string} answer + * @param {Object} behaviour Behavioral settings for the task from semantics + * @param {boolean} behaviour.acceptSpellingErrors - If true, answers will also count correct if they contain small spelling errors. + * @param {string} defaultUserAnswer + * @param {Object} l10n Localized texts + * @param {string} l10n.solutionLabel Assistive technology label for cloze solution + * @param {string} l10n.inputLabel Assistive technology label for cloze input + * @param {string} l10n.inputHasTipLabel Assistive technology label for input with tip + * @param {string} l10n.tipLabel Label for tip icon + */ + Blanks.Cloze = function (solution, behaviour, defaultUserAnswer, l10n) { + var self = this; + var $input, $wrapper; + var answers = solution.solutions; + var answer = answers.join('/'); + var tip = solution.tip; + var checkedAnswer = null; + var inputLabel = l10n.inputLabel; + + if (behaviour.caseSensitive !== true) { + // Convert possible solutions into lowercase + for (var i = 0; i < answers.length; i++) { + answers[i] = answers[i].toLowerCase(); + } + } + + /** + * Check if the answer is correct. + * + * @private + * @param {string} answered + */ + var correct = function (answered) { + if (behaviour.caseSensitive !== true) { + answered = answered.toLowerCase(); + } + for (var i = 0; i < answers.length; i++) { + // Damerau-Levenshtein comparison + if (behaviour.acceptSpellingErrors === true) { + var levenshtein = H5P.TextUtilities.computeLevenshteinDistance(answered, H5P.trim(answers[i]), true); + /* + * The correctness is temporarily computed by word length and number of number of operations + * required to change one word into the other (Damerau-Levenshtein). It's subject to + * change, cmp. https://github.com/otacke/udacity-machine-learning-engineer/blob/master/submissions/capstone_proposals/h5p_fuzzy_blanks.md + */ + if ((answers[i].length > 9) && (levenshtein <= 2)) { + return true; + } else if ((answers[i].length > 3) && (levenshtein <= 1)) { + return true; + } + } + // regular comparison + if (answered === H5P.trim(answers[i])) { + return true; + } + } + return false; + }; + + /** + * Check if filled out. + * + * @param {boolean} + */ + this.filledOut = function () { + var answered = this.getUserAnswer(); + // Blank can be correct and is interpreted as filled out. + return (answered !== '' || correct(answered)); + }; + + /** + * Check the cloze and mark it as wrong or correct. + */ + this.checkAnswer = function () { + checkedAnswer = this.getUserAnswer(); + var isCorrect = correct(checkedAnswer); + if (isCorrect) { + $wrapper.addClass('h5p-correct'); + $input.attr('disabled', true) + .attr('aria-label', inputLabel + '. ' + l10n.answeredCorrectly); + } + else { + $wrapper.addClass('h5p-wrong'); + $input.attr('aria-label', inputLabel + '. ' + l10n.answeredIncorrectly); + } + }; + + /** + * Disables input. + * @method disableInput + */ + this.disableInput = function () { + this.toggleInput(false); + }; + + /** + * Enables input. + * @method enableInput + */ + this.enableInput = function () { + this.toggleInput(true); + }; + + /** + * Toggles input enable/disable + * @method toggleInput + * @param {boolean} enabled True if input should be enabled, otherwise false + */ + this.toggleInput = function (enabled) { + $input.attr('disabled', !enabled); + }; + + /** + * Show the correct solution. + */ + this.showSolution = function () { + if (correct(this.getUserAnswer())) { + return; // Only for the wrong ones + } + + $('', { + 'aria-hidden': true, + 'class': 'h5p-correct-answer', + text: H5P.trim(answer.replace(/\s*\/\s*/g, '/')), + insertAfter: $wrapper + }); + $input.attr('disabled', true); + var ariaLabel = inputLabel + '. ' + + l10n.solutionLabel + ' ' + answer + '. ' + + l10n.answeredIncorrectly; + + $input.attr('aria-label', ariaLabel); + }; + + /** + * @returns {boolean} + */ + this.correct = function () { + return correct(this.getUserAnswer()); + }; + + /** + * Set input element. + * + * @param {H5P.jQuery} $element + * @param {function} afterCheck + * @param {function} afterFocus + * @param {number} clozeIndex Index of cloze + * @param {number} totalCloze Total amount of clozes in blanks + */ + this.setInput = function ($element, afterCheck, afterFocus, clozeIndex, totalCloze) { + $input = $element; + $wrapper = $element.parent(); + inputLabel = inputLabel.replace('@num', (clozeIndex + 1)) + .replace('@total', totalCloze); + + // Add tip if tip is set + if(tip !== undefined && tip.trim().length > 0) { + $wrapper.addClass('has-tip') + .append(H5P.JoubelUI.createTip(tip, { + tipLabel: l10n.tipLabel + })); + inputLabel += '. ' + l10n.inputHasTipLabel; + } + + $input.attr('aria-label', inputLabel); + + if (afterCheck !== undefined) { + $input.blur(function () { + if (self.filledOut()) { + // Check answers + if (!behaviour.enableRetry) { + self.disableInput(); + } + self.checkAnswer(); + afterCheck.apply(self); + } + }); + } + $input.keyup(function () { + if (checkedAnswer !== null && checkedAnswer !== self.getUserAnswer()) { + // The Answer has changed since last check + checkedAnswer = null; + $wrapper.removeClass('h5p-wrong'); + $input.attr('aria-label', inputLabel); + if (afterFocus !== undefined) { + afterFocus(); + } + } + }); + }; + + /** + * @returns {string} Cloze html + */ + this.toString = function () { + var extra = defaultUserAnswer ? ' value="' + defaultUserAnswer + '"' : ''; + var result = ''; + self.length = result.length; + return result; + }; + + /** + * @returns {string} Trimmed answer + */ + this.getUserAnswer = function () { + const trimmedAnswer = H5P.trim($input.val().replace(/\ /g, ' ')); + // Set trimmed answer + $input.val(trimmedAnswer); + if (behaviour.formulaEditor) { + // If fomula editor is enabled set trimmed text + $input.parent().find('.wiris-h5p-input').html(trimmedAnswer); + } + return trimmedAnswer; + }; + + /** + * @param {string} text New input text + */ + this.setUserInput = function (text) { + $input.val(text); + }; + + /** + * Resets aria label of input field + */ + this.resetAriaLabel = function () { + $input.attr('aria-label', inputLabel); + }; + }; + +})(H5P.jQuery, H5P.Blanks); diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/language/nb.json b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/language/nb.json new file mode 100644 index 0000000000..dbee84de69 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/language/nb.json @@ -0,0 +1,214 @@ +{ + "semantics": [ + { + "label": "Medieelement", + "fields": [ + { + "label": "Type", + "description": "Valgfritt medieelement. Elementet vil bli plassert over spørsmålet." + }, + { + "label": "Deaktiver zoomingfunksjon." + } + ] + }, + { + "label": "Oppgavebeskrivelse", + "default": "Fyll inn teksten som mangler", + "description": "En tekst som forteller brukeren hvordan oppgaven skal løses" + }, + { + "label": "Tekstlinjer", + "entity": "tekstlinje", + "field": { + "label": "Tekstlinje", + "placeholder": "Oslo er hovedstaden i *Norge*", + "important": { + "description": "
  • Åpne felter merkes med en stjerne * foran og bak det korrekte ordet/uttrykket.
  • Alternativer angis med en skråstrek (/)
  • Tips angis med et kolon (:) før tipset
", + "example": "H5P content may be edited using a *browser/web-browser:Something you use every day*." + } + } + }, + { + "label": "Tilbakemelding på hele oppgava", + "fields": [ + { + "widgets": [ + { + "label": "Forhandsinnstilling" + } + ], + "label": "Opprett poengområder og legg inn tilbakemeldinger.", + "description": "Klikk på knappen \"Legg til poengområde\" og legg til så mange poengområder du trenger. Eksempel: 0–40 % Svakt resultat, 41–80 % Gjennomsnittlig resultat, 81–100 % Flott resultat!", + "entity": "Område", + "field": { + "fields": [ + { + "label": "Poengområde" + }, + {}, + { + "label": "Tilbakemelding for definert poengområde", + "placeholder": "Skriv inn tilbakemelding." + } + ] + } + } + ] + }, + { + "label": "Tekst for \"Vis svar\" knapp", + "default": "Vis svar" + }, + { + "label": "Tekst for \"Prøv igjen\" knapp", + "default": "Prøv igjen" + }, + { + "label": "Tekst for \"Sjekk\" knapp", + "default": "Sjekk" + }, + { + "label": "Text for \"Submit\" button", + "default": "Submit" + }, + { + "label": "Text for \"Not filled out\" message", + "default": "Please fill in all blanks to view solution" + }, + { + "label": "Tekst for \"':ans' er korrekt\"-melding", + "default": "':ans' er korrekt" + }, + { + "label": "Tekst for \"':ans' er feil\"-melding", + "default": "':ans' er feil" + }, + { + "label": "Tekst for \"Svar rett\"-melding", + "default": "Svar rett" + }, + { + "label": "Tekst for \"Svar feil\"-melding", + "default": "Svar feil" + }, + { + "label": "Merkelapp for løsning brukt av tekniske hjelpemidler", + "default": "Svar:" + }, + { + "label": "Merkelapp for inntastingsfelt brukt av tekniske hjelpemidler", + "description": "Bruk @num og @total for å bytte ut feltnummer og totalt antall felter", + "default": "Felt @num av @total" + }, + { + "label": "Merkelapp for å fortelle at et inntastingsfelt har et tips knyttet til seg", + "default": "Tips tilgjengelig" + }, + { + "label": "Merkelapp for tips ikon", + "default": "Tips" + }, + { + "label": "Innstillinger for oppgave-oppførsel", + "description": "Disse instillingene lar deg bestemme hvordan oppgavetypen skal oppføre seg.", + "fields": [ + { + "label": "Aktiver \"Prøv igjen\"-knapp" + }, + { + "label": "Aktiver \"Fasit\"-knapp" + }, + { + "label": "Enable \"Check\" button" + }, + { + "label": "Gi tilbakemelding med en gang brukeren har avgitt svar" + }, + { + "label": "Skill mellom store og små bokstaver", + "description": "Sørger for at svaret til brukeren må være nøyaktig det samme som i oppgaven." + }, + { + "label": "Krev at alle felter er besvart før fasit gis" + }, + { + "label": "Sett inntastingsfelt på egne linjer" + }, + { + "label": "Slå på bruker-bekreftelse for \"Fasit\"", + "description": "Denne innstillingen er ikke forenbar med \"Gi tilbakemelding med en gang brukeren har avgitt svar\" alternativet." + }, + { + "label": "Slå på bruker-bekreftelse for \"Prøv igjen\"" + }, + { + "label": "Accept minor spelling errors", + "description": "If activated, an answer will also count as correct with minor spelling errors (3-9 characters: 1 spelling error, more than 9 characters: 2 spelling errors)" + } + ] + }, + { + "label": "Bruker-bekreftelse før fasit-visning", + "fields": [ + { + "label": "Tittel", + "default": "Ferdig ?" + }, + { + "label": "Tekst", + "default": "Er du sikker på at du er ferdig?" + }, + { + "label": "Avbryt etikett", + "default": "Avbryt" + }, + { + "label": "Bekreftelse etikett", + "default": "Bekreft" + } + ] + }, + { + "label": "Prøv igjen bruker-bekreftelse", + "fields": [ + { + "label": "Tittel", + "default": "Prøv igjen ?" + }, + { + "label": "Tekst", + "default": "Er du sikker på at du vil prøve igjen?" + }, + { + "label": "Avbryt etikett", + "default": "Avbryt" + }, + { + "label": "Bekreft etikett", + "default": "Bekreft" + } + ] + }, + { + "label": "Textual representation of the score bar for those using a readspeaker", + "default": "You got :num out of :total points" + }, + { + "label": "Assistive technology description for \"Check\" button", + "default": "Check the answers. The responses will be marked as correct, incorrect, or unanswered." + }, + { + "label": "Assistive technology description for \"Show Solution\" button", + "default": "Show the solution. The task will be marked with its correct solution." + }, + { + "label": "Assistive technology description for \"Retry\" button", + "default": "Retry the task. Reset all responses and start the task over again." + }, + { + "label": "Assistive technology description for starting task", + "default": "Checking mode" + } + ] +} diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/library.json b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/library.json new file mode 100644 index 0000000000..562429f120 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/library.json @@ -0,0 +1,65 @@ +{ + "title": "Fill in the Blanks", + "description": "Test your users with fill in the blanks tasks(Cloze tests).", + "machineName": "H5P.Blanks", + "majorVersion": 1, + "minorVersion": 14, + "patchVersion": 6, + "runnable": 1, + "license": "MIT", + "author": "Joubel", + "embedTypes": [ + "iframe" + ], + "coreApi": { + "majorVersion": 1, + "minorVersion": 19 + }, + "preloadedCss": [ + { + "path": "css/blanks.css" + } + ], + "preloadedJs": [ + { + "path": "js/blanks.js" + }, + { + "path": "js/cloze.js" + } + ], + "preloadedDependencies": [ + { + "machineName": "FontAwesome", + "majorVersion": 4, + "minorVersion": 5 + }, + { + "machineName": "H5P.Question", + "majorVersion": 1, + "minorVersion": 5 + }, + { + "machineName": "H5P.JoubelUI", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.TextUtilities", + "majorVersion": 1, + "minorVersion": 3 + } + ], + "editorDependencies": [ + { + "machineName": "H5PEditor.RangeList", + "majorVersion": 1, + "minorVersion": 0 + }, + { + "machineName": "H5PEditor.ShowWhen", + "majorVersion": 1, + "minorVersion": 0 + } + ] +} \ No newline at end of file diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/presave.js b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/presave.js new file mode 100644 index 0000000000..947c9340cc --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/presave.js @@ -0,0 +1,40 @@ +var H5PPresave = H5PPresave || {}; +/** + * Resolve the presave logic for the content type Fill in the Blanks + * + * @param {object} content + * @param finished + * @constructor + */ +H5PPresave['H5P.Blanks'] = function (content, finished) { + var presave = H5PEditor.Presave; + + if (isContentInvalid()) { + throw { + name: 'Invalid Fill in the blanks Error', + message: 'Could not find expected semantics in content.' + }; + } + + var score = content.questions + .map(function (question) { + var pattern = /\*[^\*]+\*/g; + var matches = question.match(pattern); + return Array.isArray(matches) ? matches.length : 0; + }) + .reduce(function (previous, current) { + return previous + current; + }, 0); + + presave.validateScore(score); + + finished({maxScore: score}); + + /** + * Check if required parameters is present + * @return {boolean} + */ + function isContentInvalid() { + return !presave.checkNestedRequirements(content, 'content.questions') || !Array.isArray(content.questions); + } +}; diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/semantics.json b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/semantics.json new file mode 100644 index 0000000000..8cce012481 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/semantics.json @@ -0,0 +1,489 @@ +[ + { + "name": "media", + "type": "group", + "label": "Media", + "importance": "medium", + "fields": [ + { + "name": "type", + "type": "library", + "label": "Type", + "options": [ + "H5P.Image 1.1", + "H5P.Video 1.6", + "H5P.Audio 1.5" + ], + "optional": true, + "description": "Optional media to display above the question." + }, + { + "name": "disableImageZooming", + "type": "boolean", + "label": "Disable image zooming", + "importance": "low", + "default": false, + "optional": true, + "widget": "showWhen", + "showWhen": { + "rules": [ + { + "field": "type", + "equals": "H5P.Image 1.1" + } + ] + } + } + ] + }, + { + "label": "Task description", + "importance": "high", + "name": "text", + "type": "text", + "widget": "html", + "default": "Fill in the missing words", + "description": "A guide telling the user how to answer this task.", + "enterMode": "p", + "tags": [ + "strong", + "em", + "u", + "a", + "ul", + "ol", + "h2", + "h3", + "hr", + "pre", + "code" + ] + }, + { + "name": "questions", + "type": "list", + "label": "Text blocks", + "importance": "high", + "entity": "text block", + "min": 1, + "max": 31, + "field": { + "name": "question", + "type": "text", + "widget": "html", + "label": "Line of text", + "importance": "high", + "placeholder": "Oslo is the capital of *Norway*.", + "description": "", + "important": { + "description": "
  • Blanks are added with an asterisk (*) in front and behind the correct word/phrase.
  • Alternative answers are separated with a forward slash (/).
  • You may add a textual tip, using a colon (:) in front of the tip.
", + "example": "H5P content may be edited using a *browser/web-browser:Something you use every day*." + }, + "enterMode": "p", + "tags": [ + "strong", + "em", + "del", + "u", + "code" + ] + } + }, + { + "name": "overallFeedback", + "type": "group", + "label": "Overall Feedback", + "importance": "low", + "expanded": true, + "fields": [ + { + "name": "overallFeedback", + "type": "list", + "widgets": [ + { + "name": "RangeList", + "label": "Default" + } + ], + "importance": "high", + "label": "Define custom feedback for any score range", + "description": "Click the \"Add range\" button to add as many ranges as you need. Example: 0-20% Bad score, 21-91% Average Score, 91-100% Great Score!", + "entity": "range", + "min": 1, + "defaultNum": 1, + "optional": true, + "field": { + "name": "overallFeedback", + "type": "group", + "importance": "low", + "fields": [ + { + "name": "from", + "type": "number", + "label": "Score Range", + "min": 0, + "max": 100, + "default": 0, + "unit": "%" + }, + { + "name": "to", + "type": "number", + "min": 0, + "max": 100, + "default": 100, + "unit": "%" + }, + { + "name": "feedback", + "type": "text", + "label": "Feedback for defined score range", + "importance": "low", + "placeholder": "Fill in the feedback", + "optional": true + } + ] + } + } + ] + }, + { + "label": "Text for \"Show solutions\" button", + "name": "showSolutions", + "type": "text", + "default": "Show solution", + "common": true + }, + { + "label": "Text for \"Retry\" button", + "importance": "low", + "name": "tryAgain", + "type": "text", + "default": "Retry", + "common": true, + "optional": true + }, + { + "label": "Text for \"Check\" button", + "importance": "low", + "name": "checkAnswer", + "type": "text", + "default": "Check", + "common": true, + "optional": true + }, + { + "label": "Text for \"Submit\" button", + "importance": "low", + "name": "submitAnswer", + "type": "text", + "default": "Submit", + "common": true, + "optional": true + }, + { + "label": "Text for \"Not filled out\" message", + "importance": "low", + "name": "notFilledOut", + "type": "text", + "default": "Please fill in all blanks to view solution", + "common": true, + "optional": true + }, + { + "label": "Text for \"':ans' is correct\" message", + "importance": "low", + "name": "answerIsCorrect", + "type": "text", + "default": "':ans' is correct", + "common": true, + "optional": true + }, + { + "label": "Text for \"':ans' is wrong\" message", + "importance": "low", + "name": "answerIsWrong", + "type": "text", + "default": "':ans' is wrong", + "common": true, + "optional": true + }, + { + "label": "Text for \"Answered correctly\" message", + "importance": "low", + "name": "answeredCorrectly", + "type": "text", + "default": "Answered correctly", + "common": true, + "optional": true + }, + { + "label": "Text for \"Answered incorrectly\" message", + "importance": "low", + "name": "answeredIncorrectly", + "type": "text", + "default": "Answered incorrectly", + "common": true, + "optional": true + }, + { + "label": "Assistive technology label for solution", + "importance": "low", + "name": "solutionLabel", + "type": "text", + "default": "Correct answer:", + "common": true, + "optional": true + }, + { + "label": "Assistive technology label for input field", + "importance": "low", + "name": "inputLabel", + "type": "text", + "description": "Use @num and @total to replace current cloze number and total cloze number", + "default": "Blank input @num of @total", + "common": true, + "optional": true + }, + { + "label": "Assistive technology label for saying an input has a tip tied to it", + "importance": "low", + "name": "inputHasTipLabel", + "type": "text", + "default": "Tip available", + "common": true, + "optional": true + }, + { + "label": "Tip icon label", + "importance": "low", + "name": "tipLabel", + "type": "text", + "default": "Tip", + "common": true, + "optional": true + }, + { + "name": "behaviour", + "type": "group", + "label": "Behavioural settings.", + "importance": "low", + "description": "These options will let you control how the task behaves.", + "optional": true, + "fields": [ + { + "label": "Enable \"Retry\"", + "importance": "low", + "name": "enableRetry", + "type": "boolean", + "default": true, + "optional": true + }, + { + "label": "Enable \"Show solution\" button", + "importance": "low", + "name": "enableSolutionsButton", + "type": "boolean", + "default": true, + "optional": true + }, + { + "name": "enableCheckButton", + "type": "boolean", + "label": "Enable \"Check\" button", + "widget": "none", + "importance": "low", + "default": true, + "optional": true + }, + { + "label": "Automatically check answers after input", + "importance": "low", + "name": "autoCheck", + "type": "boolean", + "default": false, + "optional": true + }, + { + "name": "caseSensitive", + "importance": "low", + "type": "boolean", + "default": true, + "label": "Case sensitive", + "description": "Makes sure the user input has to be exactly the same as the answer." + }, + { + "label": "Require all fields to be answered before the solution can be viewed", + "importance": "low", + "name": "showSolutionsRequiresInput", + "type": "boolean", + "default": true, + "optional": true + }, + { + "label": "Put input fields on separate lines", + "importance": "low", + "name": "separateLines", + "type": "boolean", + "default": false, + "optional": true + }, + { + "label": "Show confirmation dialog on \"Check\"", + "importance": "low", + "name": "confirmCheckDialog", + "type": "boolean", + "description": "This options is not compatible with the \"Automatically check answers after input\" option", + "default": false + }, + { + "label": "Show confirmation dialog on \"Retry\"", + "importance": "low", + "name": "confirmRetryDialog", + "type": "boolean", + "default": false + }, + { + "name": "acceptSpellingErrors", + "type": "boolean", + "label": "Accept minor spelling errors", + "importance": "low", + "description": "If activated, an answer will also count as correct with minor spelling errors (3-9 characters: 1 spelling error, more than 9 characters: 2 spelling errors)", + "default": false, + "optional": true + } + ] + }, + { + "label": "Check confirmation dialog", + "importance": "low", + "name": "confirmCheck", + "type": "group", + "common": true, + "fields": [ + { + "label": "Header text", + "importance": "low", + "name": "header", + "type": "text", + "default": "Finish ?" + }, + { + "label": "Body text", + "importance": "low", + "name": "body", + "type": "text", + "default": "Are you sure you wish to finish ?", + "widget": "html", + "enterMode": "p", + "tags": [ + "strong", + "em", + "del", + "u", + "code" + ] + }, + { + "label": "Cancel button label", + "importance": "low", + "name": "cancelLabel", + "type": "text", + "default": "Cancel" + }, + { + "label": "Confirm button label", + "importance": "low", + "name": "confirmLabel", + "type": "text", + "default": "Finish" + } + ] + }, + { + "label": "Retry confirmation dialog", + "importance": "low", + "name": "confirmRetry", + "type": "group", + "common": true, + "fields": [ + { + "label": "Header text", + "importance": "low", + "name": "header", + "type": "text", + "default": "Retry ?" + }, + { + "label": "Body text", + "importance": "low", + "name": "body", + "type": "text", + "default": "Are you sure you wish to retry ?", + "widget": "html", + "enterMode": "p", + "tags": [ + "strong", + "em", + "del", + "u", + "code" + ] + }, + { + "label": "Cancel button label", + "importance": "low", + "name": "cancelLabel", + "type": "text", + "default": "Cancel" + }, + { + "label": "Confirm button label", + "importance": "low", + "name": "confirmLabel", + "type": "text", + "default": "Confirm" + } + ] + }, + { + "name": "scoreBarLabel", + "type": "text", + "label": "Textual representation of the score bar for those using a readspeaker", + "default": "You got :num out of :total points", + "importance": "low", + "common": true + }, + { + "name": "a11yCheck", + "type": "text", + "label": "Assistive technology description for \"Check\" button", + "default": "Check the answers. The responses will be marked as correct, incorrect, or unanswered.", + "importance": "low", + "common": true + }, + { + "name": "a11yShowSolution", + "type": "text", + "label": "Assistive technology description for \"Show Solution\" button", + "default": "Show the solution. The task will be marked with its correct solution.", + "importance": "low", + "common": true + }, + { + "name": "a11yRetry", + "type": "text", + "label": "Assistive technology description for \"Retry\" button", + "default": "Retry the task. Reset all responses and start the task over again.", + "importance": "low", + "common": true + }, + { + "name": "a11yCheckingModeHeader", + "type": "text", + "label": "Assistive technology description for starting task", + "default": "Checking mode", + "importance": "low", + "common": true + } +] \ No newline at end of file diff --git a/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/upgrades.js b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/upgrades.js new file mode 100644 index 0000000000..bf3ea18e60 --- /dev/null +++ b/sourcecode/apis/contentauthor/tests/files/libraries/H5P.Blanks-1.14.6/upgrades.js @@ -0,0 +1,124 @@ +var H5PUpgrades = H5PUpgrades || {}; + +H5PUpgrades['H5P.Blanks'] = (function () { + return { + 1: { + 1: { + contentUpgrade: function (parameters, finished) { + // Moved all behavioural settings into "behaviour" group. + parameters.behaviour = { + enableRetry: parameters.enableTryAgain === undefined ? true : parameters.enableRetry, + enableSolutionsButton: true, + autoCheck: parameters.autoCheck === undefined ? false : parameters.autoCheck, + caseSensitive: parameters.caseSensitive === undefined ? true : parameters.caseSensitive, + showSolutionsRequiresInput: parameters.showSolutionsRequiresInput === undefined ? true : parameters.showSolutionsRequiresInput, + separateLines: parameters.separateLines === undefined ? false : parameters.separateLines + }; + delete parameters.enableTryAgain; + delete parameters.enableShowSolution; + delete parameters.autoCheck; + delete parameters.caseSensitive; + delete parameters.showSolutionsRequiresInput; + delete parameters.separateLines; + delete parameters.changeAnswer; + + finished(null, parameters); + } + }, + + /** + * Asynchronous content upgrade hook. + * Upgrades content parameters to support Blanks 1.5. + * + * Converts task image into media object, adding support for video. + * + * @params {Object} parameters + * @params {function} finished + */ + 5: function (parameters, finished) { + + if (parameters.image) { + // Convert image field to media field + parameters.media = { + library: 'H5P.Image 1.0', + params: { + file: parameters.image + } + }; + + // Remove old image field + delete parameters.image; + } + + // Done + finished(null, parameters); + }, + + /** + * Asynchronous content upgrade hook. + * Upgrades content parameters to support Blanks 1.8 + * + * Move old feedback message to the new overall feedback system. + * + * @param {object} parameters + * @param {function} finished + */ + 8: function (parameters, finished) { + if (parameters && parameters.score) { + parameters.overallFeedback = [ + { + 'from': 0, + 'to': 100, + 'feedback': parameters.score + } + ]; + + delete parameters.score; + } + + finished(null, parameters); + }, + + /** + * Asynchronous content upgrade hook. + * + * @param {[type]} parameters + * @param {[type]} finished + * @param {[type]} extras + * @return {[type]} + */ + 11: function (parameters, finished, extras) { + // Move value from getTitle() to metadata title + if (parameters && parameters.text) { + extras = extras || {}; + extras.metadata = extras.metadata || {}; + extras.metadata.title = parameters.text.replace(/<[^>]*>?/g, ''); + } + finished(null, parameters, extras); + }, + /* + * Upgrades content parameters to support Blanks 1.9 + * + * Move disableImageZooming from behaviour to media + * + * @param {object} parameters + * @param {function} finished + */ + 12: function (parameters, finished) { + // If image has been used, move it down in the hierarchy and add disableImageZooming + if (parameters && parameters.media) { + parameters.media = { + type: parameters.media, + disableImageZooming: (parameters.behaviour && parameters.behaviour.disableImageZooming) ? parameters.behaviour.disableImageZooming : false + }; + } + + // Delete old disableImageZooming + if (parameters && parameters.behaviour) { + delete parameters.behaviour.disableImageZooming; + } + finished(null, parameters); + } + } + }; +})();