diff --git a/database/factories/H5PLibraryDependencyFactory.php b/database/factories/H5PLibraryDependencyFactory.php new file mode 100644 index 00000000..786984f7 --- /dev/null +++ b/database/factories/H5PLibraryDependencyFactory.php @@ -0,0 +1,21 @@ + H5PLibrary::factory(), + 'required_library_id' => H5PLibrary::factory(), + 'dependency_type' => $this->faker->randomElement(['editor', 'preloaded']) + ]; + } +} diff --git a/database/factories/H5PLibraryFactory.php b/database/factories/H5PLibraryFactory.php index c858d479..0eb01515 100644 --- a/database/factories/H5PLibraryFactory.php +++ b/database/factories/H5PLibraryFactory.php @@ -27,6 +27,7 @@ public function definition() 'semantics' => '', 'tutorial_url' => '', 'has_icon' => 0, + 'add_to' => '' ]; } } diff --git a/database/migrations/2022_08_10_112954_add_add_to_column_to_hh5p_libraries_table.php b/database/migrations/2022_08_10_112954_add_add_to_column_to_hh5p_libraries_table.php new file mode 100644 index 00000000..86716449 --- /dev/null +++ b/database/migrations/2022_08_10_112954_add_add_to_column_to_hh5p_libraries_table.php @@ -0,0 +1,22 @@ +text('add_to')->nullable(); + }); + } + + public function down() + { + Schema::table('hh5p_libraries', function (Blueprint $table) { + $table->dropColumn('add_to'); + }); + } +}; diff --git a/src/Models/H5PLibrary.php b/src/Models/H5PLibrary.php index a2be778c..8aba5a27 100644 --- a/src/Models/H5PLibrary.php +++ b/src/Models/H5PLibrary.php @@ -170,7 +170,8 @@ class H5PLibrary extends Model 'hasIcon', 'libraryId', 'children', - 'languages' + 'languages', + 'addTo' ]; protected $appends = [ @@ -184,7 +185,8 @@ class H5PLibrary extends Model 'dropLibraryCss', 'tutorialUrl', 'hasIcon', - 'libraryId' + 'libraryId', + 'addTo' ]; protected $hidden = [ @@ -196,6 +198,7 @@ class H5PLibrary extends Model 'drop_library_css', 'tutorial_url', 'has_icon', + 'add_to' ]; public function getSemanticsAttribute($value) @@ -248,6 +251,11 @@ public function getPreloadedCssAttribute():string return isset($this->attributes['preloaded_css']) ? $this->attributes['preloaded_css'] : ''; } + public function getAddToAttribute():string + { + return isset($this->attributes['add_to']) ? $this->attributes['add_to'] : ''; + } + public function getDropLibraryCssAttribute():string { return isset($this->attributes['drop_library_css']) ? $this->attributes['drop_library_css'] : ''; @@ -265,7 +273,7 @@ public function getHasIconAttribute():string public function dependencies() { - return $this->hasMany(H5PLibraryDependency::class, 'library_id'); + return $this->hasMany(H5PLibraryDependency::class, 'library_id', 'id'); } public function children() diff --git a/src/Models/H5PLibraryDependency.php b/src/Models/H5PLibraryDependency.php index b48c4b90..cf1a6654 100644 --- a/src/Models/H5PLibraryDependency.php +++ b/src/Models/H5PLibraryDependency.php @@ -2,13 +2,17 @@ namespace EscolaLms\HeadlessH5P\Models; +use EscolaLms\HeadlessH5P\Database\Factories\H5PLibraryDependencyFactory; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use EscolaLms\HeadlessH5P\Models\H5PLibrary; -use Illuminate\Database\Eloquent\Relations\BelongsTo; + use Illuminate\Database\Eloquent\Relations\BelongsTo; class H5PLibraryDependency extends Model { + use HasFactory; + public $incrementing = false; + public $timestamps = false; protected $table = 'hh5p_libraries_dependencies'; @@ -41,4 +45,9 @@ public function requiredLibrary():BelongsTo { return $this->belongsTo(H5PLibrary::class, 'required_library_id'); } + + protected static function newFactory(): H5PLibraryDependencyFactory + { + return H5PLibraryDependencyFactory::new(); + } } diff --git a/src/Repositories/H5PEditorStorageRepository.php b/src/Repositories/H5PEditorStorageRepository.php index 1eaf29e0..dca795d9 100644 --- a/src/Repositories/H5PEditorStorageRepository.php +++ b/src/Repositories/H5PEditorStorageRepository.php @@ -26,18 +26,18 @@ class H5PEditorStorageRepository implements H5peditorStorage * @param string $lang Language code * @return string Translation in JSON format */ - public function getLanguage($machineName, $majorVersion, $minorVersion, $language): string + public function getLanguage($machineName, $majorVersion, $minorVersion, $language): ?string { if (!isset($language)) { - return ''; + return null; } + $library = H5PLibrary::select(['id'])->where([ ['major_version', $majorVersion], ['minor_version', $minorVersion], ['name', $machineName], ])->first(); - if ($library) { $libraryLanguage = H5PLibraryLanguage::where([ ['library_id', $library->id], @@ -50,7 +50,7 @@ public function getLanguage($machineName, $majorVersion, $minorVersion, $languag } } - return ''; + return null; } /** diff --git a/src/Repositories/H5PRepository.php b/src/Repositories/H5PRepository.php index 62483d5c..10c40bd4 100644 --- a/src/Repositories/H5PRepository.php +++ b/src/Repositories/H5PRepository.php @@ -17,6 +17,7 @@ use Illuminate\Support\Facades\DB; use DateTime; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Storage; class H5PRepository implements H5PFrameworkInterface { @@ -181,6 +182,8 @@ public function t($message, $replacements = []): string */ public function getLibraryFileUrl($libraryFolderName, $fileName) { + $path = 'h5p/libraries/' . $libraryFolderName . '/' . $fileName; + return Storage::disk('local')->exists($path) ? Storage::disk('local')->url($path) : null; } /** @@ -222,10 +225,25 @@ public function getUploadedH5pPath() * * @return array */ - public function loadAddons() + public function loadAddons(): array { - // TODO this should return something - return []; + return H5PLibrary::query() + ->select(['l1.id', 'l1.name', 'l1.major_version', 'l1.minor_version', 'l1.patch_version', 'l1.preloaded_js', 'l1.preloaded_css', 'l1.add_to']) + ->from('hh5p_libraries as l1') + ->leftJoin('hh5p_libraries as l2', fn($join) => $join + ->on('l1.name', '=', 'l2.name') + ->on(fn($query) => $query + ->on('l1.major_version', '<', 'l2.major_version') + ->orOn(fn ($query) => $query + ->orOn('l1.major_version', '=', 'l2.major_version') + ->on('l1.minor_version', '<', 'l2.minor_version') + ) + ) + ) + ->whereNotNull('l1.add_to') + ->whereNull('l2.name') + ->get() + ->toArray(); } /** @@ -262,6 +280,7 @@ public function loadLibraries() ->orderBy('major_version', 'ASC') ->orderBy('minor_version', 'ASC') ->get(); + $libraries = []; foreach ($results as $library) { $libraries[$library->name][] = $library; @@ -376,8 +395,6 @@ public function isInDevMode() */ public function mayUpdateLibraries() { - // TODO check if current user is allowed to do this action - // best option use middleware on router return true; } @@ -453,8 +470,9 @@ public function saveLibraryData(&$libraryData, $new = true) 'preloaded_css' => $this->pathsToCsv($libraryData, 'preloadedCss'), 'drop_library_css' => '', // TODO, what is this ? 'semantics' => isset($libraryData['semantics']) ? $libraryData['semantics'] : '', - 'tutorial_url' => isset($libraryData['tutorial_url']) ? isset($libraryData['tutorial_url']) : '', + 'tutorial_url' => isset($libraryData['tutorial_url']) ?: '', 'has_icon' => isset($libraryData['hasIcon']) ? 1 : 0, + 'add_to' => isset($library['addTo']) ? json_encode($library['addTo']) : null ]; $libObj = H5PLibrary::firstOrCreate($library); @@ -496,7 +514,7 @@ public function saveLibraryData(&$libraryData, $new = true) */ public function insertContent($content, $contentMainId = null) { - return $this->updateContent($content, $contentMainId = null); + return $this->updateContent($content, $contentMainId); } private function fixContentParamsMetadataLibraryTitle($content) @@ -659,6 +677,7 @@ public function saveLibraryDependencies($libraryId, $dependencies, $dependency_t */ public function copyLibraryUsage($contentId, $copyFromId, $contentMainId = null) { + } /** @@ -679,6 +698,7 @@ public function deleteContentData($contentId) */ public function deleteLibraryUsage($contentId) { + H5PContent::findOrFail($contentId)->libraries()->delete(); } /** @@ -868,7 +888,7 @@ public function alterLibrarySemantics(&$semantics, $machineName, $majorVersion, */ public function deleteLibraryDependencies($libraryId) { - // TODO this must be implemented + H5PLibrary::findOrFail($libraryId)->dependencies()->delete(); } /** @@ -1053,6 +1073,7 @@ public function setOption($name, $value) */ public function updateContentFields($id, $fields) { + H5PContent::findOrFail($id)->update($fields); } /** @@ -1064,6 +1085,7 @@ public function updateContentFields($id, $fields) */ public function clearFilteredParameters($library_ids) { + H5PContent::query()->whereIn('library_id', $library_ids)->update(['filtered' => null]); } /** @@ -1119,6 +1141,7 @@ public function getLibraryStats($type) */ public function getNumAuthors() { + return H5PContent::query()->select(['user_id'])->distinct()->count('user_id'); } /** @@ -1220,16 +1243,8 @@ public function hasPermission($permission, $id = null) */ public function replaceContentTypeCache($contentTypeCache) { - - // TODO refactor this ugly code - // Replace existing content type cache - DB::table('hh5p_libraries_hub_cache')->truncate(); - - // TODO wrap this in a transaction - foreach ($contentTypeCache->contentTypes as $ct) { - // Insert into db - H5pLibrariesHubCache::create([ + $data[] = [ 'machine_name' => $ct->id, 'major_version' => $ct->version->major, 'minor_version' => $ct->version->minor, @@ -1245,14 +1260,19 @@ public function replaceContentTypeCache($contentTypeCache) 'is_recommended' => $ct->isRecommended === true ? 1 : 0, 'popularity' => $ct->popularity, 'screenshots' => json_encode($ct->screenshots), - 'license' => json_encode(isset($ct->license) ? $ct->license : []), + 'license' => json_encode($ct->license ?? []), 'example' => $ct->example, - 'tutorial' => isset($ct->tutorial) ? $ct->tutorial : '', - 'keywords' => json_encode(isset($ct->keywords) ? $ct->keywords : []), - 'categories' => json_encode(isset($ct->categories) ? $ct->categories : []), + 'tutorial' => $ct->tutorial ?? '', + 'keywords' => json_encode($ct->keywords ?? []), + 'categories' => json_encode($ct->categories ?? []), 'owner' => $ct->owner, - ]); + ]; } + + DB::transaction(function () use($data) { + H5pLibrariesHubCache::query()->delete(); + H5pLibrariesHubCache::insert($data); + }); } /** diff --git a/tests/Fixture/H5PContentTypeFixture.php b/tests/Fixture/H5PContentTypeFixture.php new file mode 100644 index 00000000..6a820649 --- /dev/null +++ b/tests/Fixture/H5PContentTypeFixture.php @@ -0,0 +1,85 @@ +data = new stdClass(); + } + + public static function fixture(): H5PContentTypeFixture + { + return new H5PContentTypeFixture(); + } + + public function count(int $count = 1): self + { + $this->count = $count; + + return $this; + } + + public function make(): self + { + if ($this->count <= 1) { + $this->data->contentTypes = $this->factory(); + + return $this; + } + + for ($i = 0; $i < $this->count; $i++) + { + $this->data->contentTypes[] = $this->factory(); + } + + return $this; + } + + public function get(): stdClass + { + return $this->data; + } + + private function factory(): stdClass + { + $faker = FakerFactory::create(); + + $data = new stdClass(); + $data->id = $faker->word; + $data->version = new stdClass(); + $data->version->major = $faker->numberBetween(1, 10); + $data->version->minor = $faker->numberBetween(1, 10); + $data->version->patch = $faker->numberBetween(1, 10); + $data->coreApiVersionNeeded = new stdClass(); + $data->coreApiVersionNeeded->major = $faker->numberBetween(1, 10); + $data->coreApiVersionNeeded->minor = $faker->numberBetween(1, 10); + $data->title = $faker->word; + $data->summary = $faker->words(3, true); + $data->description = $faker->words(3, true); + $data->icon = $faker->url; + $data->createdAt = Carbon::now()->toISOString(); + $data->updatedAt = Carbon::now()->toISOString(); + $data->isRecommended = $faker->boolean; + $data->popularity = $faker->numberBetween(1, 10); + $data->screenshots = $faker->url; + $data->license = array(); + $data->example = $faker->word; + $data->tutorial = $faker->url; + $data->keywords = array($faker->word); + $data->categories = array($faker->word); + $data->owner = $faker->firstName . ' ' . $faker->lastName; + + return $data; + } +} diff --git a/tests/Repositories/H5PRepositoryTest.php b/tests/Repositories/H5PRepositoryTest.php index 59d5ef40..eea3f8a5 100644 --- a/tests/Repositories/H5PRepositoryTest.php +++ b/tests/Repositories/H5PRepositoryTest.php @@ -2,10 +2,16 @@ namespace EscolaLms\HeadlessH5P\Tests\Repositories; +use EscolaLms\HeadlessH5P\Models\H5PContent; +use EscolaLms\HeadlessH5P\Models\H5PContentLibrary; +use EscolaLms\HeadlessH5P\Models\H5PLibrary; +use EscolaLms\HeadlessH5P\Models\H5PLibraryDependency; use EscolaLms\HeadlessH5P\Repositories\H5PRepository; +use EscolaLms\HeadlessH5P\Tests\Fixture\H5PContentTypeFixture; use EscolaLms\HeadlessH5P\Tests\TestCase; use GuzzleHttp\Psr7\Response; use H5PHubEndpoints; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Testing\DatabaseTransactions; class H5PRepositoryTest extends TestCase @@ -73,4 +79,145 @@ public function testFetchExternalDataShouldReturnFalseWhenStatusIsDifferentFrom2 $this->assertFalse($result); } + + public function testLoadAddonsShouldReturnHigherVersionLibrary(): void + { + H5PLibrary::factory()->create(['name' => 'Test', 'major_version' => 1, 'minor_version' => 2, 'add_to' => 'sth']); + $library1 = H5PLibrary::factory()->create(['name' => 'Test', 'major_version' => 1, 'minor_version' => 3, 'add_to' => 'sth']); + + $result = $this->repository->loadAddons(); + + $this->assertCount(1, $result); + $this->assertEquals($library1->getKey(), $result[0]['id']); + $this->assertEquals(3, $result[0]['minorVersion']); + $this->assertEquals(1, $result[0]['majorVersion']); + } + + public function testLoadAddonsShouldReturnHigherMajorVersionLibrary(): void + { + $library1 = H5PLibrary::factory()->create(['name' => 'Test', 'major_version' => 2, 'minor_version' => 1, 'add_to' => 'sth']); + H5PLibrary::factory()->create(['name' => 'Test', 'major_version' => 1, 'minor_version' => 1, 'add_to' => 'sth']); + + $result = $this->repository->loadAddons(); + + $this->assertCount(1, $result); + $this->assertEquals($library1->getKey(), $result[0]['id']); + $this->assertEquals(1, $result[0]['minorVersion']); + $this->assertEquals(2, $result[0]['majorVersion']); + } + + public function testLoadAddonsShouldReturnAllLibrariesWhenNamesIsNotSame(): void + { + H5PLibrary::factory()->create(['name' => 'Test1', 'major_version' => 2, 'minor_version' => 1, 'add_to' => 'sth']); + H5PLibrary::factory()->create(['name' => 'Test2', 'major_version' => 1, 'minor_version' => 3, 'add_to' => 'sth']); + + $result = $this->repository->loadAddons(); + + $this->assertCount(2, $result); + } + + public function testDeleteLibraryUsageShouldDeleteLibrariesById(): void + { + $h5pContent = H5PContent::factory() + ->has(H5PContentLibrary::factory()->count(5), 'libraries') + ->create(['library_id' => H5PLibrary::factory()->create()->getKey()]); + + $this->assertCount(5, $h5pContent->libraries); + + $this->repository->deleteLibraryUsage($h5pContent->getKey()); + + $this->assertCount(0, H5PContent::find($h5pContent->getKey())->libraries); + } + + public function testDeleteLibraryUsageShouldFailWhenContentNotExists(): void + { + $this->expectException(ModelNotFoundException::class); + $this->repository->deleteLibraryUsage(1); + } + + public function testDeleteLibraryDependenciesShouldDeleteAllDependencies(): void + { + $library = H5PLibrary::factory() + ->has(H5PLibraryDependency::factory(), 'dependencies') + ->create(); + + $this->assertCount(1, $library->dependencies); + + $this->repository->deleteLibraryDependencies($library->getKey()); + + $this->assertCount(0, H5PLibrary::find($library->getKey())->dependencies); + } + + public function testDeleteLibraryDependenciesShouldFailWhenLibraryNotExists(): void + { + $this->expectException(ModelNotFoundException::class); + $this->repository->deleteLibraryDependencies(123); + } + + public function testUpdateContentFieldsShouldUpdateSlugAndFilteredFields(): void + { + $h5pContent = H5PContent::factory()->create(['library_id' => H5PLibrary::factory()->create()->getKey()]); + + $this->repository->updateContentFields($h5pContent->getKey(), ['slug' => 'slug-123', 'filtered' => '{"test": "test"}']); + + $result = H5PContent::find($h5pContent->getKey()); + $this->assertEquals('slug-123', $result->slug); + $this->assertEquals('{"test": "test"}', $result->filtered); + $this->assertNotEquals($h5pContent->slug, $result->slug); + $this->assertNotEquals($h5pContent->filtered, $result->filtered); + } + + public function testClearFilteredParametersShouldClearFilteredField(): void + { + $libraryId1 = H5PLibrary::factory()->create()->getKey(); + $libraryId2 = H5PLibrary::factory()->create()->getKey(); + $h5pContent1 = H5PContent::factory()->create(['library_id' => $libraryId1, 'filtered' => '{"foo": "bar"']); + $h5pContent2 = H5PContent::factory()->create(['library_id' => $libraryId1, 'filtered' => '{"foo": "bar"']); + $h5pContent3 = H5PContent::factory()->create(['library_id' => $libraryId2, 'filtered' => '{"foo": "bar"']); + + $this->assertNotNull($h5pContent1->filtered); + $this->assertNotNull($h5pContent2->filtered); + $this->assertNotNull($h5pContent3->filtered); + + $this->repository->clearFilteredParameters([$libraryId1]); + + $this->assertNull(H5PContent::find($h5pContent1->getKey())->filtered); + $this->assertNull(H5PContent::find($h5pContent2->getKey())->filtered); + $this->assertNotNull(H5PContent::find($h5pContent3->getKey())->filtered); + } + + + public function testGetNumAuthors(): void + { + H5PContent::factory()->create(['library_id' => 1, 'user_id' => 1]); + H5PContent::factory()->create(['library_id' => 1, 'user_id' => 12]); + + $result = $this->repository->getNumAuthors(); + + $this->assertEquals(2, $result); + } + + public function testReplaceContentTypeCacheShouldCreateContentType(): void + { + $data = H5PContentTypeFixture::fixture()->count(3)->make()->get(); + + $this->assertDatabaseCount('hh5p_libraries_hub_cache', 0); + + $this->repository->replaceContentTypeCache($data); + + $this->assertDatabaseCount('hh5p_libraries_hub_cache', 3); + } + + public function testReplaceContentTypeCacheShouldTruncateExistingDataAndCreateContentType(): void + { + $data = H5PContentTypeFixture::fixture()->count(3)->make()->get(); + $this->repository->replaceContentTypeCache($data); + + $this->assertDatabaseCount('hh5p_libraries_hub_cache', 3); + + $data = H5PContentTypeFixture::fixture()->count(10)->make()->get(); + $this->repository->replaceContentTypeCache($data); + + $this->assertDatabaseCount('hh5p_libraries_hub_cache', 10); + } }