Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use JsonException;
use Ramsey\Uuid\Uuid;
Expand All @@ -42,21 +43,27 @@ public function index(Request $request): View
$contents = collect();
$count = 0;

$fromLibrary = H5PLibrary::where('name', 'H5P.NDLAThreeImage')
$fromLibraries = H5PLibrary::whereIn('name', ['H5P.NDLAThreeImage', 'H5P.ThreeImage'])
->where('major_version', 0)
->where('minor_version', 5)
->first();
->get()
->keyBy('name');
$toLibrary = H5PLibrary::where('name', 'H5P.EscapeRoom')
->where('major_version', 0)
->where('minor_version', 7)
->first();

if ($fromLibrary !== null && $toLibrary !== null) {
if ($fromLibraries->isNotEmpty() && $toLibrary !== null) {
if ($request->method() === 'POST' && $request->has('content')) {
$migrated = $this->migrate($fromLibrary, $toLibrary, $request->input('content'));
$migrated = $this->migrate($fromLibraries, $toLibrary, $request->input('content'));
}

$leaves = $this->getLeaves();
$leaves = $fromLibraries
->flatMap(function (H5PLibrary $fromLibrary) {
return collect($this->getLeaves($fromLibrary->name))
->map(fn (array $leaf) => $leaf + ['source_library' => $fromLibrary->name]);
})
->values();
$routePrefix = route('h5p.ltishow', '') . '/';

// Extract H5P IDs from the leaf launch URLs
Expand All @@ -72,7 +79,7 @@ public function index(Request $request): View
->forget('');

// Single query to fetch all matching H5P content
$h5pContents = H5PContent::where('library_id', $fromLibrary->id)
$h5pContents = H5PContent::whereIn('library_id', $fromLibraries->pluck('id'))
->whereIn('id', $leafsByH5pId->keys())
->get()
->keyBy('id');
Expand All @@ -84,6 +91,7 @@ public function index(Request $request): View
return (object)[
'h5p_id' => $h5pContent->id,
'title' => $leaf['title'] ?: $h5pContent->title,
'source_library' => $leaf['source_library'],
'hub_content_id' => $leaf['content_id'],
'update_url' => $leaf['update_url'],
];
Expand All @@ -94,7 +102,7 @@ public function index(Request $request): View
}

return view('admin.migrate.index', [
'fromLibrary' => $fromLibrary,
'fromLibraries' => $fromLibraries,
'toLibrary' => $toLibrary,
'migrated' => $migrated ?? [],
'paginator' => (new LengthAwarePaginator($contents, $count, $pageSize))
Expand All @@ -103,12 +111,14 @@ public function index(Request $request): View
}

/**
* @param Collection<int|string, H5PLibrary> $fromLibraries
* @param array<array{h5p_id: string, update_url: string}> $items
*/
private function migrate(H5pLibrary $fromLibrary, H5pLibrary $toLibrary, array $items): array
private function migrate(Collection $fromLibraries, H5PLibrary $toLibrary, array $items): array
{
$migrated = [];
$runId = Uuid::uuid4()->toString();
$librariesById = $fromLibraries->keyBy('id');

foreach ($items as $item) {
$h5pId = $item['h5p_id'] ?? null;
Expand All @@ -118,8 +128,11 @@ private function migrate(H5pLibrary $fromLibrary, H5pLibrary $toLibrary, array $
continue;
}

$sourceH5p = H5PContent::where('id', $h5pId)->where('library_id', $fromLibrary->id)->first();
$sourceH5p = H5PContent::where('id', $h5pId)
->whereIn('library_id', $librariesById->keys())
->first();
if ($sourceH5p !== null) {
$fromLibrary = $librariesById[$sourceH5p->library_id];
$logData = [
'runId' => $runId,
'fromLibrary' => [
Expand Down Expand Up @@ -156,7 +169,7 @@ private function migrate(H5pLibrary $fromLibrary, H5pLibrary $toLibrary, array $
} finally {
$migrated[$sourceH5p->id] = $result;
AuditLog::log(
'Migrate content from H5P.NDLAThreeImage to H5P.EscapeRoom',
'Migrate content from ' . $fromLibrary->name . ' to ' . $toLibrary->name,
json_encode($logData),
);
}
Expand All @@ -175,31 +188,164 @@ private function alterParameters(string $parameters, string $libraryName): strin
if ($libraryName == 'H5P.ThreeImage') {
$content['threeImage']['wasConvertedFromVirtualTour'] = true;
}
for ($i = 0; $i < count($content["threeImage"]["scenes"] ?? []); $i++) {
$content["threeImage"]["scenes"][$i]["enableZoom"] = true;
/*
* From code at https://github.com/NDLANO/h5p-vt2er/blob/c11a34b9cdaa6842c1430a79912e78531ca21bcb/h5p-vt2er/app/H5PVT2ER.php#L299
* May or may not be interested in adding this as well
for ($j = 0; $j < count($content["threeImage"]["scenes"][$i]["interactions"] ?? []); $j++) {
$content["threeImage"]["scenes"][$i]["interactions"][$j]["iconTypeTextBox"] = "text-icon";
$content["threeImage"]["scenes"][$i]["interactions"][$j]["showAsHotspot"] = false;
$content["threeImage"]["scenes"][$i]["interactions"][$j]["showAsOpenSceneContent"] = false;
}
*/
if (isset($content["threeImage"]["scenes"])) {
$scenes = $content["threeImage"]["scenes"];
$scenes = $this->migrateScenes($scenes, $libraryName);
$content["threeImage"]["scenes"] = $scenes;
}
foreach ($this->l10nDefaults($libraryName) as $key => $translation) {
$content["l10n"][$key] = $content["l10n"][$key] ?? $translation;
}

return json_encode($content, flags: JSON_THROW_ON_ERROR);
}

/**
* Enable zoom for scenes, and for H5P.ThreeImage (Virtual Tour) content adapt the interactions
* to the layout Escape Room uses for converted content.
*
* @param array $scenes
*
* @return array
*/
private function migrateScenes(array $scenes, string $libraryName): array
{
for ($i = 0; $i < count($scenes); $i++) {
$scenes[$i]["enableZoom"] = true;

if ($libraryName == 'H5P.ThreeImage') {
for ($j = 0; $j < count($scenes[$i]["interactions"] ?? []); $j++) {
$scenes[$i]["interactions"][$j]["iconTypeTextBox"] = "text-icon";
$scenes[$i]["interactions"][$j]["showAsHotspot"] = false;
$scenes[$i]["interactions"][$j]["showAsOpenSceneContent"] = false;
}
}
}

if ($libraryName == 'H5P.NDLAThreeImage') {
$scenes = $this->fixHotspotInteractionPositions($scenes);
}

return $scenes;
}

/**
* Compensate for `transform: translate(-50%, -50%)` set on "nav buttons" shown as hotspot other than
* GoToScene in static scenes and panorama scenes.
*
* @param array $scenes
*
* @return array
*/
private function fixHotspotInteractionPositions(array $scenes): array
{
foreach ($scenes as $sceneIndex => $scene) {
if (($scene["sceneType"] ?? null) === "360" || !isset($scene["interactions"])) {
continue;
}

foreach ($scene["interactions"] as $interactionIndex => $interaction) {
if (
($interaction["showAsHotspot"] ?? null) !== true ||
!isset($interaction["action"]["library"]) ||
$interaction["action"]["library"] === "H5P.GoToScene 0.1"
) {
continue;
}

$positions = isset($interaction["interactionpos"])
? explode(",", $interaction["interactionpos"])
: [];
$sizes = isset($interaction["hotspotSettings"]["hotSpotSizeValues"])
? explode(",", $interaction["hotspotSettings"]["hotSpotSizeValues"])
: [];

if (count($positions) < 2 || count($sizes) < 2) {
continue;
}
// `interactionpos` is something like "1.234%,5.678%"
$left = (float)trim($positions[0]);
$top = (float)trim($positions[1]);

// `hotSpotSizeValues` is something like "1.234,5.678"
$width = (float)$sizes[0];
$height = (float)$sizes[1];

$scenes[$sceneIndex]["interactions"][$interactionIndex]["interactionpos"] =
($left + $width / 2) . "%," . ($top + $height / 2) . "%";
}
}

return $scenes;
}

/**
* Default translations for Escape Room UI strings that Virtual Tour content does not have yet.
*
* @return array<string, string>
*/
private function l10nDefaults(string $libraryName): array
{
$defaults = [
"buttonZoomIn" => "Zoom in",
"buttonZoomOut" => "Zoom out",
"zoomToolbar" => "Zoom toolbar",
"zoomAriaLabel" => "num% zoomed in",
];

if ($libraryName == 'H5P.ThreeImage') {
return [
"title" => "Title",
"playAudioTrack" => "Play audio track",
"pauseAudioTrack" => "Pause audio track",
"sceneDescription" => "Scene description",
"resetCamera" => "Reset camera",
"submitDialog" => "Submit dialog",
"closeDialog" => "Close dialog",
"expandButtonAriaLabel" => "Expand the visual label",
"backgroundLoading" => "Loading background image ...",
"noContent" => "No content",
"goToScene" => "Go to scene",
"edit" => "Edit",
"delete" => "Delete",
"score" => "Score",
"assignment" => "Assignment",
"total" => "Total",
"scoreSummary" => "Show score summary",
"scene" => "Scene",
"untitled" => "Untitled",
"userIsAtStartScene" => "You are at the start scene",
"unlocked" => "Unlocked",
"locked" => "Locked",
"searchRoomForCode" => "Search the room until you find the code",
"wrongCode" => "The code was wrong, try again.",
"contentUnlocked" => "The content has been unlocked!",
"code" => "Code",
"lockedStateAction" => "Unlock",
"hotspotDragHorizAlt" => "Drag horizontally to scale",
"hotspotDragVertiAlt" => "Drag vertically to scale",
"hint" => "Hint",
"lockedContent" => "Locked content",
"back" => "Back",
"buttonFullscreenEnter" => "Enter fullscreen mode",
"buttonFullscreenExit" => "Exit fullscreen mode",
"mainToolbar" => "Main toolbar",
"noValidSceneSet" => "No valid scenes have been set.",
] + $defaults;
}

return $defaults;
}

/**
* @return array<array{lti_launch_url: string, title: string, content_id: string, update_url: string}>
* @throws GuzzleException|JsonException|RuntimeException
*/
private function getLeaves(): array
private function getLeaves(string $machineName): array
{
$decoded = $this->hubClient->post(
'/content-versions/leaves',
['tag' => 'h5p:h5p.ndlathreeimage'],
['tag' => 'h5p:h5p.' . strtolower($machineName)],
);

return $decoded['data'] ?? [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3>
Migrate the contents of library <code>H5P.NDLAThreeImage 0.5.x</code> to <code>H5P.EscapeRoom 0.7.x</code>
Migrate the contents of libraries <code>H5P.NDLAThreeImage 0.5.x</code> and <code>H5P.ThreeImage 0.5.x</code> to <code>H5P.EscapeRoom 0.7.x</code>
</h3>
</div>
<div class="panel-body">
H5P.NDLAThreeImage 0.5.x: @if($fromLibrary) <a href="{{ route('admin.check-library', [$fromLibrary['id']]) }}" target="_blank">{{ $fromLibrary->getLibraryString(true) }}</a> @else <code>Not found</code> @endif
<p>
@foreach(['H5P.NDLAThreeImage', 'H5P.ThreeImage'] as $libraryName)
{{ $libraryName }} 0.5.x: @if($library = $fromLibraries->get($libraryName)) <a href="{{ route('admin.check-library', [$library['id']]) }}" target="_blank">{{ $library->getLibraryString(true) }}</a> @else <code>Not found</code> @endif
<p>
@endforeach
H5P.EscapeRoom 0.7.x: @if($toLibrary) <a href="{{ route('admin.check-library', [$toLibrary['id']]) }}" target="_blank">{{ $toLibrary->getLibraryString(true) }}</a> @else <code>Not found</code> @endif
</div>
@if ($toLibrary && $fromLibrary)
@if ($toLibrary && $fromLibraries->isNotEmpty())
@if (count($migrated) > 0)
<div class="panel-body">
<h4>Migrated content</h4>
Expand Down Expand Up @@ -56,6 +58,7 @@
<th>Migrate</th>
<th>Content id</th>
<th>Title</th>
<th>Library</th>
</tr>
</thead>
<tbody>
Expand All @@ -67,13 +70,13 @@
</td>
<td><a href="{{ route('admin.content-details', [$content->h5p_id]) }}">{{ $content->h5p_id }}</a></td>
<td>{{ $content->title }}</td>
<td>{{ $content->source_library }}</td>
</tr>
@endforeach
</tbody>
</table>

@csrf
<input type="hidden" value="{{ $fromLibrary->id }}">
<div id="form-group major-publishing-actions" class="submitbox">
<input type="submit" name="submit" value="Migrate content" class="button button-primary button-large btn btn-primary"/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion sourcecode/apis/contentauthor/routes/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function () {

Route::match(['GET', 'POST'], 'ajax', [AdminController::class, 'ajaxLoading'])->name('admin.ajax');

// Migrate content from H5P.NDLAThreeImage to H5P.EscapeRoom
// Migrate content from H5P.NDLAThreeImage and H5P.ThreeImage to H5P.EscapeRoom
Route::match(['GET', 'POST'], '/migrate/library-content', [AdminContentMigrateController::class, 'index'])
->name('admin.migrate.library-content');

Expand Down
Loading