refactor(php): rename SongGroup references throughout controllers/services/tests
Replace all SongGroup/SongArrangementGroup model usages with Label/SongArrangementLabel after the schema migration to global labels. Updates 12 app files and 11 test files: - SongService: createDefaultGroups now finds-or-creates global Labels by name - ArrangementController: validates label_id (labels are global, no song-ownership) - ProImportService: imports groups as Labels (firstOrCreate by name); does not overwrite existing label colors per spec - ProExportService/SongPdfController/TranslationService/etc: traverse via arrangements -> arrangementLabels -> label -> songSlides chain - All test factories and assertions adapted to label-based schema
This commit is contained in:
parent
a1612dc3ef
commit
bdbf0c65e3
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongArrangement;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ArrangementController extends Controller
|
||||
{
|
||||
|
|
@ -23,17 +23,24 @@ public function store(Request $request, Song $song): RedirectResponse
|
|||
'is_default' => false,
|
||||
]);
|
||||
|
||||
$groups = $song->groups()->orderBy('order')->get();
|
||||
$rows = $groups->map(fn ($group, $index) => [
|
||||
$defaultArr = $song->arrangements()->where('is_default', true)->first();
|
||||
|
||||
if ($defaultArr === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$arrangementLabels = $defaultArr->arrangementLabels()->orderBy('order')->get();
|
||||
|
||||
$rows = $arrangementLabels->values()->map(fn ($al, $index) => [
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $al->label_id,
|
||||
'order' => $index + 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
])->all();
|
||||
|
||||
if ($rows !== []) {
|
||||
$arrangement->arrangementGroups()->insert($rows);
|
||||
$arrangement->arrangementLabels()->insert($rows);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -47,14 +54,14 @@ public function clone(Request $request, SongArrangement $arrangement): RedirectR
|
|||
]);
|
||||
|
||||
DB::transaction(function () use ($arrangement, $data): void {
|
||||
$arrangement->loadMissing('arrangementGroups');
|
||||
$arrangement->loadMissing('arrangementLabels');
|
||||
|
||||
$clone = $arrangement->song->arrangements()->create([
|
||||
'name' => $data['name'],
|
||||
'is_default' => false,
|
||||
]);
|
||||
|
||||
$this->cloneGroups($arrangement, $clone);
|
||||
$this->cloneArrangementLabels($arrangement, $clone);
|
||||
});
|
||||
|
||||
return back()->with('success', 'Arrangement wurde geklont.');
|
||||
|
|
@ -64,33 +71,22 @@ public function update(Request $request, SongArrangement $arrangement): Redirect
|
|||
{
|
||||
$data = $request->validate([
|
||||
'groups' => ['array'],
|
||||
'groups.*.song_group_id' => ['required', 'integer', 'exists:song_groups,id'],
|
||||
'groups.*.label_id' => ['required', 'integer', 'exists:labels,id'],
|
||||
'groups.*.order' => ['required', 'integer', 'min:1'],
|
||||
'group_colors' => ['sometimes', 'array'],
|
||||
'group_colors.*' => ['required', 'string', 'regex:/^#[0-9A-Fa-f]{6}$/'],
|
||||
]);
|
||||
|
||||
$groupIds = collect($data['groups'] ?? [])->pluck('song_group_id')->values();
|
||||
$uniqueGroupIds = $groupIds->unique()->values();
|
||||
$labelIds = collect($data['groups'] ?? [])->pluck('label_id')->values();
|
||||
|
||||
$validGroupIds = $arrangement->song->groups()
|
||||
->whereIn('id', $uniqueGroupIds)
|
||||
->pluck('id');
|
||||
DB::transaction(function () use ($arrangement, $labelIds, $data): void {
|
||||
$arrangement->arrangementLabels()->delete();
|
||||
|
||||
if ($uniqueGroupIds->count() !== $validGroupIds->count()) {
|
||||
throw ValidationException::withMessages([
|
||||
'groups' => 'Du kannst nur Gruppen aus diesem Song verwenden.',
|
||||
]);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($arrangement, $groupIds, $data): void {
|
||||
$arrangement->arrangementGroups()->delete();
|
||||
|
||||
$rows = $groupIds
|
||||
$rows = $labelIds
|
||||
->values()
|
||||
->map(fn (int $songGroupId, int $index) => [
|
||||
->map(fn (int $labelId, int $index) => [
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $songGroupId,
|
||||
'label_id' => $labelId,
|
||||
'order' => $index + 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
|
|
@ -98,14 +94,12 @@ public function update(Request $request, SongArrangement $arrangement): Redirect
|
|||
->all();
|
||||
|
||||
if ($rows !== []) {
|
||||
$arrangement->arrangementGroups()->insert($rows);
|
||||
$arrangement->arrangementLabels()->insert($rows);
|
||||
}
|
||||
|
||||
if (! empty($data['group_colors'])) {
|
||||
foreach ($data['group_colors'] as $groupId => $color) {
|
||||
$arrangement->song->groups()
|
||||
->whereKey((int) $groupId)
|
||||
->update(['color' => $color]);
|
||||
foreach ($data['group_colors'] as $labelId => $color) {
|
||||
Label::whereKey((int) $labelId)->update(['color' => $color]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -136,28 +130,28 @@ public function destroy(SongArrangement $arrangement): RedirectResponse
|
|||
return back()->with('success', 'Arrangement wurde gelöscht.');
|
||||
}
|
||||
|
||||
private function cloneGroups(?SongArrangement $source, SongArrangement $target): void
|
||||
private function cloneArrangementLabels(?SongArrangement $source, SongArrangement $target): void
|
||||
{
|
||||
if ($source === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$groups = $source->arrangementGroups
|
||||
$arrangementLabels = $source->arrangementLabels
|
||||
->sortBy('order')
|
||||
->values();
|
||||
|
||||
$rows = $groups
|
||||
->map(fn ($arrangementGroup) => [
|
||||
$rows = $arrangementLabels
|
||||
->map(fn ($arrangementLabel) => [
|
||||
'song_arrangement_id' => $target->id,
|
||||
'song_group_id' => $arrangementGroup->song_group_id,
|
||||
'order' => $arrangementGroup->order,
|
||||
'label_id' => $arrangementLabel->label_id,
|
||||
'order' => $arrangementLabel->order,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
])
|
||||
->all();
|
||||
|
||||
if ($rows !== []) {
|
||||
$target->arrangementGroups()->insert($rows);
|
||||
$target->arrangementLabels()->insert($rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ public function importPro(Request $request): JsonResponse
|
|||
|
||||
public function downloadPro(Song $song): BinaryFileResponse
|
||||
{
|
||||
if ($song->groups()->count() === 0) {
|
||||
if ($this->countSongLabels($song) === 0) {
|
||||
abort(422, 'Song hat keine Gruppen oder Slides zum Exportieren.');
|
||||
}
|
||||
|
||||
|
|
@ -60,4 +60,12 @@ public function downloadPro(Song $song): BinaryFileResponse
|
|||
|
||||
return response()->download($tempPath, $filename)->deleteFileAfterSend(true);
|
||||
}
|
||||
|
||||
private function countSongLabels(Song $song): int
|
||||
{
|
||||
return $song->arrangements()
|
||||
->withCount('arrangementLabels')
|
||||
->get()
|
||||
->sum('arrangement_labels_count');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,15 +129,13 @@ public function edit(Service $service): Response
|
|||
$service->load([
|
||||
'serviceSongs' => fn ($query) => $query->orderBy('order'),
|
||||
'serviceSongs.song',
|
||||
'serviceSongs.song.groups',
|
||||
'serviceSongs.song.arrangements.arrangementGroups.group',
|
||||
'serviceSongs.song.arrangements.arrangementLabels.label',
|
||||
'serviceSongs.arrangement',
|
||||
'slides',
|
||||
'agendaItems' => fn ($q) => $q->orderBy('sort_order'),
|
||||
'agendaItems.slides',
|
||||
'agendaItems.serviceSong.song.groups.slides',
|
||||
'agendaItems.serviceSong.song.arrangements.arrangementGroups.group',
|
||||
'agendaItems.serviceSong.arrangement.arrangementGroups.group',
|
||||
'agendaItems.serviceSong.song.arrangements.arrangementLabels.label.songSlides',
|
||||
'agendaItems.serviceSong.arrangement.arrangementLabels.label',
|
||||
]);
|
||||
|
||||
$songsCatalog = Song::query()
|
||||
|
|
@ -253,15 +251,7 @@ public function edit(Service $service): Response
|
|||
'title' => $ss->song->title,
|
||||
'ccli_id' => $ss->song->ccli_id,
|
||||
'has_translation' => $ss->song->has_translation,
|
||||
'groups' => $ss->song->groups
|
||||
->sortBy('order')
|
||||
->values()
|
||||
->map(fn ($group) => [
|
||||
'id' => $group->id,
|
||||
'name' => $group->name,
|
||||
'color' => $group->color,
|
||||
'order' => $group->order,
|
||||
]),
|
||||
'groups' => $this->collectSongLabels($ss->song),
|
||||
'arrangements' => $ss->song->arrangements
|
||||
->sortBy(fn ($arrangement) => $arrangement->is_default ? 0 : 1)
|
||||
->values()
|
||||
|
|
@ -269,13 +259,13 @@ public function edit(Service $service): Response
|
|||
'id' => $arrangement->id,
|
||||
'name' => $arrangement->name,
|
||||
'is_default' => $arrangement->is_default,
|
||||
'groups' => $arrangement->arrangementGroups
|
||||
'groups' => $arrangement->arrangementLabels
|
||||
->sortBy('order')
|
||||
->values()
|
||||
->map(fn ($arrangementGroup) => [
|
||||
'id' => $arrangementGroup->group?->id,
|
||||
'name' => $arrangementGroup->group?->name,
|
||||
'color' => $arrangementGroup->group?->color,
|
||||
->map(fn ($arrangementLabel) => [
|
||||
'id' => $arrangementLabel->label?->id,
|
||||
'name' => $arrangementLabel->label?->name,
|
||||
'color' => $arrangementLabel->label?->color,
|
||||
])
|
||||
->filter(fn ($group) => $group['id'] !== null)
|
||||
->values(),
|
||||
|
|
@ -412,4 +402,25 @@ public function downloadAgendaItem(Service $service, ServiceAgendaItem $agendaIt
|
|||
)
|
||||
->deleteFileAfterSend(true);
|
||||
}
|
||||
|
||||
private function collectSongLabels(Song $song): \Illuminate\Support\Collection
|
||||
{
|
||||
$defaultArr = $song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first();
|
||||
|
||||
if ($defaultArr === null) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $defaultArr->arrangementLabels
|
||||
->sortBy('order')
|
||||
->values()
|
||||
->map(fn ($arrangementLabel) => [
|
||||
'id' => $arrangementLabel->label?->id,
|
||||
'name' => $arrangementLabel->label?->name,
|
||||
'color' => $arrangementLabel->label?->color,
|
||||
'order' => $arrangementLabel->order,
|
||||
])
|
||||
->filter(fn ($group) => $group['id'] !== null)
|
||||
->values();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@ public function __construct(
|
|||
private readonly SongService $songService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Alle Songs auflisten (paginiert, durchsuchbar).
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Song::query();
|
||||
|
|
@ -53,15 +50,11 @@ public function index(Request $request): JsonResponse
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Neuen Song erstellen mit Default-Gruppen und -Arrangement.
|
||||
*/
|
||||
public function store(SongRequest $request): JsonResponse
|
||||
{
|
||||
$song = DB::transaction(function () use ($request) {
|
||||
$song = Song::create($request->validated());
|
||||
|
||||
$this->songService->createDefaultGroups($song);
|
||||
$this->songService->createDefaultArrangement($song);
|
||||
|
||||
return $song;
|
||||
|
|
@ -69,16 +62,13 @@ public function store(SongRequest $request): JsonResponse
|
|||
|
||||
return response()->json([
|
||||
'message' => 'Song erfolgreich erstellt',
|
||||
'data' => $this->formatSongDetail($song->fresh(['groups.slides', 'arrangements.arrangementGroups'])),
|
||||
'data' => $this->formatSongDetail($song->fresh(['arrangements.arrangementLabels.label.songSlides'])),
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Song mit Gruppen, Slides und Arrangements anzeigen.
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$song = Song::with(['groups.slides', 'arrangements.arrangementGroups'])->find($id);
|
||||
$song = Song::with(['arrangements.arrangementLabels.label.songSlides'])->find($id);
|
||||
|
||||
if (! $song) {
|
||||
return response()->json(['message' => 'Song nicht gefunden'], 404);
|
||||
|
|
@ -89,9 +79,6 @@ public function show(int $id): JsonResponse
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Song-Metadaten aktualisieren.
|
||||
*/
|
||||
public function update(SongRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$song = Song::find($id);
|
||||
|
|
@ -104,13 +91,10 @@ public function update(SongRequest $request, int $id): JsonResponse
|
|||
|
||||
return response()->json([
|
||||
'message' => 'Song erfolgreich aktualisiert',
|
||||
'data' => $this->formatSongDetail($song->fresh(['groups.slides', 'arrangements.arrangementGroups'])),
|
||||
'data' => $this->formatSongDetail($song->fresh(['arrangements.arrangementLabels.label.songSlides'])),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Song soft-löschen.
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$song = Song::find($id);
|
||||
|
|
@ -126,11 +110,35 @@ public function destroy(int $id): JsonResponse
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Song-Detail formatieren.
|
||||
*/
|
||||
private function formatSongDetail(Song $song): array
|
||||
{
|
||||
$defaultArr = $song->arrangements->firstWhere('is_default', true);
|
||||
|
||||
$groupsPayload = [];
|
||||
if ($defaultArr !== null) {
|
||||
$groupsPayload = $defaultArr->arrangementLabels
|
||||
->sortBy('order')
|
||||
->values()
|
||||
->map(fn ($al) => [
|
||||
'id' => $al->label?->id,
|
||||
'name' => $al->label?->name,
|
||||
'color' => $al->label?->color,
|
||||
'order' => $al->order,
|
||||
'slides' => $al->label
|
||||
? $al->label->songSlides
|
||||
->sortBy('order')
|
||||
->values()
|
||||
->map(fn ($slide) => [
|
||||
'id' => $slide->id,
|
||||
'order' => $slide->order,
|
||||
'text_content' => $slide->text_content,
|
||||
'text_content_translated' => $slide->text_content_translated,
|
||||
'notes' => $slide->notes,
|
||||
])->toArray()
|
||||
: [],
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $song->id,
|
||||
'title' => $song->title,
|
||||
|
|
@ -144,27 +152,15 @@ private function formatSongDetail(Song $song): array
|
|||
'last_used_in_service' => $song->last_used_in_service,
|
||||
'created_at' => $song->created_at->toDateTimeString(),
|
||||
'updated_at' => $song->updated_at->toDateTimeString(),
|
||||
'groups' => $song->groups->sortBy('order')->values()->map(fn ($group) => [
|
||||
'id' => $group->id,
|
||||
'name' => $group->name,
|
||||
'color' => $group->color,
|
||||
'order' => $group->order,
|
||||
'slides' => $group->slides->sortBy('order')->values()->map(fn ($slide) => [
|
||||
'id' => $slide->id,
|
||||
'order' => $slide->order,
|
||||
'text_content' => $slide->text_content,
|
||||
'text_content_translated' => $slide->text_content_translated,
|
||||
'notes' => $slide->notes,
|
||||
])->toArray(),
|
||||
])->toArray(),
|
||||
'groups' => $groupsPayload,
|
||||
'arrangements' => $song->arrangements->map(fn ($arr) => [
|
||||
'id' => $arr->id,
|
||||
'name' => $arr->name,
|
||||
'is_default' => $arr->is_default,
|
||||
'arrangement_groups' => $arr->arrangementGroups->sortBy('order')->values()->map(fn ($ag) => [
|
||||
'id' => $ag->id,
|
||||
'song_group_id' => $ag->song_group_id,
|
||||
'order' => $ag->order,
|
||||
'arrangement_groups' => $arr->arrangementLabels->sortBy('order')->values()->map(fn ($al) => [
|
||||
'id' => $al->id,
|
||||
'label_id' => $al->label_id,
|
||||
'order' => $al->order,
|
||||
])->toArray(),
|
||||
])->toArray(),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -57,21 +57,25 @@ public function download(Song $song, SongArrangement $arrangement): Response|Jso
|
|||
private function buildGroupsInOrder(SongArrangement $arrangement): array
|
||||
{
|
||||
$arrangement->load([
|
||||
'arrangementGroups' => fn ($query) => $query->orderBy('order'),
|
||||
'arrangementGroups.group.slides' => fn ($query) => $query->orderBy('order'),
|
||||
'arrangementLabels' => fn ($query) => $query->orderBy('order'),
|
||||
'arrangementLabels.label.songSlides' => fn ($query) => $query->orderBy('order'),
|
||||
]);
|
||||
|
||||
return $arrangement->arrangementGroups->map(function ($arrangementGroup) {
|
||||
$group = $arrangementGroup->group;
|
||||
return $arrangement->arrangementLabels->map(function ($arrangementLabel) {
|
||||
$label = $arrangementLabel->label;
|
||||
|
||||
if ($label === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $group->name,
|
||||
'color' => $group->color ?? '#6b7280',
|
||||
'slides' => $group->slides->map(fn ($slide) => [
|
||||
'name' => $label->name,
|
||||
'color' => $label->color ?? '#6b7280',
|
||||
'slides' => $label->songSlides->map(fn ($slide) => [
|
||||
'text_content' => $slide->text_content,
|
||||
'text_content_translated' => $slide->text_content_translated,
|
||||
])->values()->all(),
|
||||
];
|
||||
})->values()->all();
|
||||
})->filter()->values()->all();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,41 +18,48 @@ public function __construct(
|
|||
public function page(Song $song): Response
|
||||
{
|
||||
$song->load([
|
||||
'groups' => fn ($query) => $query
|
||||
->orderBy('order')
|
||||
->with([
|
||||
'slides' => fn ($slideQuery) => $slideQuery->orderBy('order'),
|
||||
]),
|
||||
'arrangements' => fn ($q) => $q->where('is_default', true),
|
||||
'arrangements.arrangementLabels' => fn ($q) => $q->orderBy('order'),
|
||||
'arrangements.arrangementLabels.label.songSlides',
|
||||
]);
|
||||
|
||||
$defaultArr = $song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first();
|
||||
|
||||
$groups = collect();
|
||||
if ($defaultArr !== null) {
|
||||
$groups = $defaultArr->arrangementLabels
|
||||
->sortBy('order')
|
||||
->values()
|
||||
->map(fn ($al) => [
|
||||
'id' => $al->label?->id,
|
||||
'name' => $al->label?->name,
|
||||
'color' => $al->label?->color,
|
||||
'order' => $al->order,
|
||||
'slides' => $al->label
|
||||
? $al->label->songSlides
|
||||
->sortBy('order')
|
||||
->values()
|
||||
->map(fn ($slide) => [
|
||||
'id' => $slide->id,
|
||||
'order' => $slide->order,
|
||||
'text_content' => $slide->text_content,
|
||||
'text_content_translated' => $slide->text_content_translated,
|
||||
])->values()
|
||||
: collect(),
|
||||
]);
|
||||
}
|
||||
|
||||
return Inertia::render('Songs/Translate', [
|
||||
'song' => [
|
||||
'id' => $song->id,
|
||||
'title' => $song->title,
|
||||
'ccli_id' => $song->ccli_id,
|
||||
'has_translation' => $song->has_translation,
|
||||
'groups' => $song->groups->map(fn ($group) => [
|
||||
'id' => $group->id,
|
||||
'name' => $group->name,
|
||||
'color' => $group->color,
|
||||
'order' => $group->order,
|
||||
'slides' => $group->slides->map(fn ($slide) => [
|
||||
'id' => $slide->id,
|
||||
'order' => $slide->order,
|
||||
'text_content' => $slide->text_content,
|
||||
'text_content_translated' => $slide->text_content_translated,
|
||||
])->values(),
|
||||
])->values(),
|
||||
'groups' => $groups,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL abrufen und Text zum Prüfen zurückgeben.
|
||||
*
|
||||
* Der Text wird NICHT automatisch gespeichert — der Benutzer
|
||||
* prüft ihn zuerst und importiert dann explizit.
|
||||
*/
|
||||
public function fetchUrl(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
|
|
@ -72,11 +79,6 @@ public function fetchUrl(Request $request): JsonResponse
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Übersetzungstext für einen Song importieren.
|
||||
*
|
||||
* Verteilt den Text zeilenweise auf die Slides des Songs.
|
||||
*/
|
||||
public function import(int $songId, Request $request): JsonResponse
|
||||
{
|
||||
$song = Song::find($songId);
|
||||
|
|
@ -98,9 +100,6 @@ public function import(int $songId, Request $request): JsonResponse
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Übersetzung eines Songs komplett entfernen.
|
||||
*/
|
||||
public function destroy(int $songId): JsonResponse
|
||||
{
|
||||
$song = Song::find($songId);
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ public function __construct(
|
|||
private readonly ?Closure $songFetcher = null,
|
||||
private readonly ?Closure $agendaFetcher = null,
|
||||
private readonly ?Closure $eventServiceFetcher = null,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function sync(): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ public function generatePlaylist(Service $service): array
|
|||
$agendaItems = ServiceAgendaItem::where('service_id', $service->id)
|
||||
->where('is_before_event', false)
|
||||
->orderBy('sort_order')
|
||||
->with(['slides' => fn ($q) => $q->whereNull('deleted_at')->orderBy('sort_order'), 'serviceSong.song.groups.slides', 'serviceSong.arrangement.arrangementGroups.group'])
|
||||
->with([
|
||||
'slides' => fn ($q) => $q->whereNull('deleted_at')->orderBy('sort_order'),
|
||||
'serviceSong.song.arrangements.arrangementLabels.label.songSlides',
|
||||
'serviceSong.arrangement.arrangementLabels.label',
|
||||
])
|
||||
->get();
|
||||
|
||||
if ($agendaItems->isEmpty()) {
|
||||
|
|
@ -80,7 +84,7 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda
|
|||
if ($serviceSong->song_id && $serviceSong->song) {
|
||||
$song = $serviceSong->song;
|
||||
|
||||
if ($song->groups()->count() === 0) {
|
||||
if ($this->countSongLabels($song) === 0) {
|
||||
$skippedUnmatched++;
|
||||
|
||||
continue;
|
||||
|
|
@ -161,12 +165,12 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda
|
|||
*/
|
||||
private function generatePlaylistLegacy(Service $service): array
|
||||
{
|
||||
$service->loadMissing('serviceSongs.song.groups.slides');
|
||||
$service->loadMissing('serviceSongs.song.arrangements.arrangementLabels.label.songSlides');
|
||||
|
||||
$matchedSongs = $service->serviceSongs()
|
||||
->whereNotNull('song_id')
|
||||
->orderBy('order')
|
||||
->with('song.groups.slides')
|
||||
->with('song.arrangements.arrangementLabels.label.songSlides')
|
||||
->get();
|
||||
|
||||
$skippedUnmatched = $service->serviceSongs()->whereNull('song_id')->count();
|
||||
|
|
@ -191,7 +195,7 @@ private function generatePlaylistLegacy(Service $service): array
|
|||
foreach ($matchedSongs as $serviceSong) {
|
||||
$song = $serviceSong->song;
|
||||
|
||||
if (! $song || $song->groups()->count() === 0) {
|
||||
if (! $song || $this->countSongLabels($song) === 0) {
|
||||
$skippedEmpty++;
|
||||
|
||||
continue;
|
||||
|
|
@ -366,6 +370,14 @@ protected function writeProFile(string $path, string $name, array $groups, array
|
|||
ProFileGenerator::generateAndWrite($path, $name, $groups, $arrangements);
|
||||
}
|
||||
|
||||
private function countSongLabels(\App\Models\Song $song): int
|
||||
{
|
||||
return $song->arrangements()
|
||||
->withCount('arrangementLabels')
|
||||
->get()
|
||||
->sum('arrangement_labels_count');
|
||||
}
|
||||
|
||||
protected function writePlaylistFile(string $path, string $name, array $items, array $embeddedFiles): void
|
||||
{
|
||||
ProPlaylistGenerator::generateAndWrite($path, $name, $items, $embeddedFiles);
|
||||
|
|
|
|||
|
|
@ -35,8 +35,7 @@ public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string
|
|||
{
|
||||
$agendaItem->loadMissing([
|
||||
'slides',
|
||||
'serviceSong.song.groups.slides',
|
||||
'serviceSong.song.arrangements.arrangementGroups.group',
|
||||
'serviceSong.song.arrangements.arrangementLabels.label.songSlides',
|
||||
]);
|
||||
|
||||
$title = $agendaItem->title ?: 'Ablauf-Element';
|
||||
|
|
@ -44,7 +43,12 @@ public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string
|
|||
if ($agendaItem->serviceSong?->song_id && $agendaItem->serviceSong->song) {
|
||||
$song = $agendaItem->serviceSong->song;
|
||||
|
||||
if ($song->groups()->count() === 0) {
|
||||
$labelCount = $song->arrangements()
|
||||
->withCount('arrangementLabels')
|
||||
->get()
|
||||
->sum('arrangement_labels_count');
|
||||
|
||||
if ($labelCount === 0) {
|
||||
throw new RuntimeException('Song "'.$song->title.'" hat keine Gruppen.');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public function generateProFile(Song $song): string
|
|||
|
||||
public function generateParserSong(Song $song): \ProPresenter\Parser\Song
|
||||
{
|
||||
$song->loadMissing(['groups.slides', 'arrangements.arrangementGroups.group']);
|
||||
$song->loadMissing(['arrangements.arrangementLabels.label.songSlides']);
|
||||
|
||||
return ProFileGenerator::generate(
|
||||
$song->title,
|
||||
|
|
@ -37,14 +37,34 @@ public function generateParserSong(Song $song): \ProPresenter\Parser\Song
|
|||
|
||||
private function buildGroups(Song $song): array
|
||||
{
|
||||
$groups = [];
|
||||
$defaultArr = $song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first();
|
||||
|
||||
if ($defaultArr === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$defaultArr->loadMissing('arrangementLabels.label.songSlides');
|
||||
|
||||
$macroData = $this->buildMacroData();
|
||||
$groups = [];
|
||||
$seenLabelIds = [];
|
||||
|
||||
foreach ($song->groups->sortBy('order') as $group) {
|
||||
foreach ($defaultArr->arrangementLabels->sortBy('order') as $arrangementLabel) {
|
||||
$label = $arrangementLabel->label;
|
||||
|
||||
if ($label === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($label->id, $seenLabelIds, true)) {
|
||||
continue;
|
||||
}
|
||||
$seenLabelIds[] = $label->id;
|
||||
|
||||
$isCopyrightGroup = strcasecmp($label->name, 'COPYRIGHT') === 0;
|
||||
$slides = [];
|
||||
$isCopyrightGroup = strcasecmp($group->name, 'COPYRIGHT') === 0;
|
||||
|
||||
foreach ($group->slides->sortBy('order') as $slide) {
|
||||
foreach ($label->songSlides->sortBy('order') as $slide) {
|
||||
$slideData = ['text' => $slide->text_content ?? ''];
|
||||
|
||||
if ($slide->text_content_translated) {
|
||||
|
|
@ -59,8 +79,8 @@ private function buildGroups(Song $song): array
|
|||
}
|
||||
|
||||
$groups[] = [
|
||||
'name' => $group->name,
|
||||
'color' => ProImportService::hexToRgba($group->color),
|
||||
'name' => $label->name,
|
||||
'color' => ProImportService::hexToRgba($label->color ?? '#808080'),
|
||||
'slides' => $slides,
|
||||
];
|
||||
}
|
||||
|
|
@ -88,12 +108,13 @@ private function buildMacroData(): ?array
|
|||
private function buildArrangements(Song $song): array
|
||||
{
|
||||
$arrangements = [];
|
||||
$groupIdToName = $song->groups->pluck('name', 'id')->toArray();
|
||||
|
||||
foreach ($song->arrangements as $arrangement) {
|
||||
$groupNames = $arrangement->arrangementGroups
|
||||
$arrangement->loadMissing('arrangementLabels.label');
|
||||
|
||||
$groupNames = $arrangement->arrangementLabels
|
||||
->sortBy('order')
|
||||
->map(fn ($ag) => $groupIdToName[$ag->song_group_id] ?? null)
|
||||
->map(fn ($al) => $al->label?->name)
|
||||
->filter()
|
||||
->values()
|
||||
->toArray();
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongArrangement;
|
||||
use App\Models\SongArrangementGroup;
|
||||
use App\Models\SongGroup;
|
||||
use App\Models\SongArrangementLabel;
|
||||
use App\Support\MacroColorConverter;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use ProPresenter\Parser\ProFileReader;
|
||||
|
|
@ -103,28 +104,30 @@ private function upsertSong(ProSong $proSong): Song
|
|||
}
|
||||
|
||||
$song->arrangements()->each(function (SongArrangement $arr) {
|
||||
$arr->arrangementGroups()->delete();
|
||||
$arr->arrangementLabels()->delete();
|
||||
});
|
||||
$song->arrangements()->delete();
|
||||
$song->groups()->each(function (SongGroup $group) {
|
||||
$group->slides()->delete();
|
||||
});
|
||||
$song->groups()->delete();
|
||||
|
||||
$hasTranslation = false;
|
||||
$groupMap = [];
|
||||
$labelsByName = [];
|
||||
|
||||
foreach ($proSong->getGroups() as $position => $proGroup) {
|
||||
foreach ($proSong->getGroups() as $proGroup) {
|
||||
$groupName = $proGroup->getName();
|
||||
$existingLabel = Label::whereRaw('LOWER(name) = ?', [strtolower($groupName)])->first();
|
||||
|
||||
if ($existingLabel === null) {
|
||||
$color = $proGroup->getColor();
|
||||
$hexColor = $color ? self::rgbaToHex($color) : '#808080';
|
||||
$hexColor = MacroColorConverter::fromRgba($color) ?? '#808080';
|
||||
|
||||
$songGroup = $song->groups()->create([
|
||||
'name' => $proGroup->getName(),
|
||||
$existingLabel = Label::create([
|
||||
'name' => $groupName,
|
||||
'color' => $hexColor,
|
||||
'order' => $position,
|
||||
]);
|
||||
}
|
||||
|
||||
$groupMap[$proGroup->getName()] = $songGroup;
|
||||
$labelsByName[$groupName] = $existingLabel;
|
||||
|
||||
$existingLabel->songSlides()->delete();
|
||||
|
||||
foreach ($proSong->getSlidesForGroup($proGroup) as $slidePosition => $proSlide) {
|
||||
$translatedText = null;
|
||||
|
|
@ -134,7 +137,7 @@ private function upsertSong(ProSong $proSong): Song
|
|||
$hasTranslation = true;
|
||||
}
|
||||
|
||||
$songGroup->slides()->create([
|
||||
$existingLabel->songSlides()->create([
|
||||
'order' => $slidePosition,
|
||||
'text_content' => $proSlide->getPlainText(),
|
||||
'text_content_translated' => $translatedText,
|
||||
|
|
@ -153,19 +156,19 @@ private function upsertSong(ProSong $proSong): Song
|
|||
$groupsInArrangement = $proSong->getGroupsForArrangement($proArrangement);
|
||||
|
||||
foreach ($groupsInArrangement as $order => $proGroup) {
|
||||
$songGroup = $groupMap[$proGroup->getName()] ?? null;
|
||||
$label = $labelsByName[$proGroup->getName()] ?? null;
|
||||
|
||||
if ($songGroup) {
|
||||
SongArrangementGroup::create([
|
||||
if ($label) {
|
||||
SongArrangementLabel::create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $songGroup->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => $order,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $song->fresh(['groups.slides', 'arrangements.arrangementGroups']);
|
||||
return $song->fresh(['arrangements.arrangementLabels.label.songSlides']);
|
||||
}
|
||||
|
||||
public static function rgbaToHex(array $rgba): string
|
||||
|
|
|
|||
|
|
@ -2,36 +2,48 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongArrangement;
|
||||
use App\Models\SongArrangementGroup;
|
||||
use App\Models\SongGroup;
|
||||
use App\Models\SongArrangementLabel;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SongService
|
||||
{
|
||||
/**
|
||||
* Default-Gruppen für ein neues Lied erstellen.
|
||||
* Sicherstellen, dass die Default-Labels (Strophe 1, Refrain, Bridge) global existieren.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, SongGroup>
|
||||
* @return Collection<int, Label>
|
||||
*/
|
||||
public function createDefaultGroups(Song $song): \Illuminate\Database\Eloquent\Collection
|
||||
public function createDefaultGroups(Song $song): Collection
|
||||
{
|
||||
$defaults = [
|
||||
['name' => 'Strophe 1', 'color' => '#3B82F6', 'order' => 1],
|
||||
['name' => 'Refrain', 'color' => '#10B981', 'order' => 2],
|
||||
['name' => 'Bridge', 'color' => '#F59E0B', 'order' => 3],
|
||||
['name' => 'Strophe 1', 'color' => '#3B82F6'],
|
||||
['name' => 'Refrain', 'color' => '#10B981'],
|
||||
['name' => 'Bridge', 'color' => '#F59E0B'],
|
||||
];
|
||||
|
||||
foreach ($defaults as $groupData) {
|
||||
$song->groups()->create($groupData);
|
||||
$labels = collect();
|
||||
|
||||
foreach ($defaults as $data) {
|
||||
$existing = Label::whereRaw('LOWER(name) = ?', [strtolower($data['name'])])->first();
|
||||
|
||||
if ($existing === null) {
|
||||
$existing = Label::create([
|
||||
'name' => $data['name'],
|
||||
'color' => $data['color'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $song->groups()->orderBy('order')->get();
|
||||
$labels->push($existing);
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard "Normal"-Arrangement mit allen Gruppen erstellen.
|
||||
* Standard "Normal"-Arrangement mit den Default-Labels erstellen.
|
||||
*/
|
||||
public function createDefaultArrangement(Song $song): SongArrangement
|
||||
{
|
||||
|
|
@ -40,16 +52,16 @@ public function createDefaultArrangement(Song $song): SongArrangement
|
|||
'is_default' => true,
|
||||
]);
|
||||
|
||||
$groups = $song->groups()->orderBy('order')->get();
|
||||
$labels = $this->createDefaultGroups($song);
|
||||
|
||||
foreach ($groups as $index => $group) {
|
||||
$arrangement->arrangementGroups()->create([
|
||||
'song_group_id' => $group->id,
|
||||
foreach ($labels->values() as $index => $label) {
|
||||
$arrangement->arrangementLabels()->create([
|
||||
'label_id' => $label->id,
|
||||
'order' => $index + 1,
|
||||
]);
|
||||
}
|
||||
|
||||
return $arrangement->load('arrangementGroups');
|
||||
return $arrangement->load('arrangementLabels.label');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -63,15 +75,15 @@ public function duplicateArrangement(SongArrangement $arrangement, string $name)
|
|||
$clone->is_default = false;
|
||||
$clone->save();
|
||||
|
||||
foreach ($arrangement->arrangementGroups()->orderBy('order')->get() as $group) {
|
||||
SongArrangementGroup::create([
|
||||
foreach ($arrangement->arrangementLabels()->orderBy('order')->get() as $arrangementLabel) {
|
||||
SongArrangementLabel::create([
|
||||
'song_arrangement_id' => $clone->id,
|
||||
'song_group_id' => $group->song_group_id,
|
||||
'order' => $group->order,
|
||||
'label_id' => $arrangementLabel->label_id,
|
||||
'order' => $arrangementLabel->order,
|
||||
]);
|
||||
}
|
||||
|
||||
return $clone->load('arrangementGroups');
|
||||
return $clone->load('arrangementLabels.label');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,6 @@
|
|||
|
||||
class TranslationService
|
||||
{
|
||||
/**
|
||||
* Text von einer URL abrufen (Best-Effort).
|
||||
*
|
||||
* HTML-Tags werden entfernt, nur reiner Text zurückgegeben.
|
||||
* Bei Fehlern wird null zurückgegeben, ohne Exception.
|
||||
*/
|
||||
public function fetchFromUrl(string $url): ?string
|
||||
{
|
||||
try {
|
||||
|
|
@ -33,29 +27,30 @@ public function fetchFromUrl(string $url): ?string
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Übersetzungstext auf Slides verteilen, basierend auf der Zeilenanzahl jeder Slide.
|
||||
*
|
||||
* Für jede Gruppe (nach order sortiert) und jede Slide (nach order sortiert):
|
||||
* Nimm so viele Zeilen aus dem übersetzten Text, wie die Original-Slide Zeilen hat.
|
||||
*
|
||||
* Beispiel:
|
||||
* Slide 1 hat 4 Zeilen → bekommt die nächsten 4 Zeilen der Übersetzung
|
||||
* Slide 2 hat 2 Zeilen → bekommt die nächsten 2 Zeilen
|
||||
* Slide 3 hat 4 Zeilen → bekommt die nächsten 4 Zeilen
|
||||
*/
|
||||
public function importTranslation(Song $song, string $text): void
|
||||
{
|
||||
$translatedLines = explode("\n", $text);
|
||||
$offset = 0;
|
||||
|
||||
// Alle Gruppen nach order sortiert laden, mit Slides
|
||||
$groups = $song->groups()->orderBy('order')->with([
|
||||
'slides' => fn ($query) => $query->orderBy('order'),
|
||||
])->get();
|
||||
$defaultArr = $song->arrangements()
|
||||
->where('is_default', true)
|
||||
->with(['arrangementLabels' => fn ($q) => $q->orderBy('order'), 'arrangementLabels.label.songSlides'])
|
||||
->first();
|
||||
|
||||
foreach ($groups as $group) {
|
||||
foreach ($group->slides as $slide) {
|
||||
if ($defaultArr === null) {
|
||||
$this->markAsTranslated($song);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($defaultArr->arrangementLabels->sortBy('order') as $arrangementLabel) {
|
||||
$label = $arrangementLabel->label;
|
||||
|
||||
if ($label === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($label->songSlides->sortBy('order') as $slide) {
|
||||
$originalLineCount = count(explode("\n", $slide->text_content ?? ''));
|
||||
$chunk = array_slice($translatedLines, $offset, $originalLineCount);
|
||||
$offset += $originalLineCount;
|
||||
|
|
@ -69,30 +64,25 @@ public function importTranslation(Song $song, string $text): void
|
|||
$this->markAsTranslated($song);
|
||||
}
|
||||
|
||||
/**
|
||||
* Song als "hat Übersetzung" markieren.
|
||||
*/
|
||||
public function markAsTranslated(Song $song): void
|
||||
{
|
||||
$song->update(['has_translation' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Übersetzung eines Songs komplett entfernen.
|
||||
*
|
||||
* Löscht alle text_content_translated Felder und setzt has_translation auf false.
|
||||
*/
|
||||
public function removeTranslation(Song $song): void
|
||||
{
|
||||
// Alle Slides des Songs über die Gruppen aktualisieren
|
||||
$slideIds = SongSlide::whereIn(
|
||||
'song_group_id',
|
||||
$song->groups()->pluck('id')
|
||||
)->pluck('id');
|
||||
$labelIds = $song->arrangements()
|
||||
->with('arrangementLabels')
|
||||
->get()
|
||||
->flatMap(fn ($arr) => $arr->arrangementLabels->pluck('label_id'))
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
SongSlide::whereIn('id', $slideIds)->update([
|
||||
if ($labelIds->isNotEmpty()) {
|
||||
SongSlide::whereIn('label_id', $labelIds)->update([
|
||||
'text_content_translated' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$song->update(['has_translation' => false]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceAgendaItem;
|
||||
use App\Models\ServiceSong;
|
||||
use App\Models\Slide;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongGroup;
|
||||
use App\Models\SongArrangement;
|
||||
use App\Models\SongArrangementLabel;
|
||||
use App\Models\SongSlide;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
|
@ -102,8 +104,18 @@ public function test_song_agenda_item_liefert_probundle_mit_pro_daten(): void
|
|||
$service = Service::factory()->create();
|
||||
|
||||
$song = Song::factory()->create(['title' => 'Amazing Grace']);
|
||||
$group = SongGroup::factory()->create(['song_id' => $song->id, 'name' => 'Verse 1', 'order' => 1]);
|
||||
SongSlide::factory()->create(['song_group_id' => $group->id, 'text_content' => 'Amazing grace how sweet the sound', 'order' => 1]);
|
||||
$label = Label::factory()->create(['name' => 'Verse 1']);
|
||||
SongSlide::factory()->create(['label_id' => $label->id, 'text_content' => 'Amazing grace how sweet the sound', 'order' => 1]);
|
||||
|
||||
$arrangement = SongArrangement::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'is_default' => true,
|
||||
]);
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$serviceSong = ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongArrangement;
|
||||
use App\Models\SongArrangementGroup;
|
||||
use App\Models\SongGroup;
|
||||
use App\Models\SongArrangementLabel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
|
@ -34,19 +34,19 @@ public function test_create_arrangement_uses_default_song_group_ordering(): void
|
|||
|
||||
$this->assertNotNull($newArrangement);
|
||||
|
||||
$defaultGroupOrder = SongGroup::query()
|
||||
->where('song_id', $song->id)
|
||||
$defaultLabelOrder = SongArrangementLabel::query()
|
||||
->where('song_arrangement_id', $normal->id)
|
||||
->orderBy('order')
|
||||
->pluck('id')
|
||||
->pluck('label_id')
|
||||
->all();
|
||||
|
||||
$newGroups = SongArrangementGroup::query()
|
||||
$newLabels = SongArrangementLabel::query()
|
||||
->where('song_arrangement_id', $newArrangement->id)
|
||||
->orderBy('order')
|
||||
->pluck('song_group_id')
|
||||
->pluck('label_id')
|
||||
->all();
|
||||
|
||||
$this->assertSame($defaultGroupOrder, $newGroups);
|
||||
$this->assertSame($defaultLabelOrder, $newLabels);
|
||||
}
|
||||
|
||||
public function test_clone_arrangement_duplicates_current_arrangement_groups(): void
|
||||
|
|
@ -69,19 +69,19 @@ public function test_clone_arrangement_duplicates_current_arrangement_groups():
|
|||
$this->assertNotNull($clone);
|
||||
$this->assertFalse($clone->is_default);
|
||||
|
||||
$originalGroups = SongArrangementGroup::query()
|
||||
$originalLabels = SongArrangementLabel::query()
|
||||
->where('song_arrangement_id', $normal->id)
|
||||
->orderBy('order')
|
||||
->pluck('song_group_id')
|
||||
->pluck('label_id')
|
||||
->all();
|
||||
|
||||
$cloneGroups = SongArrangementGroup::query()
|
||||
$cloneLabels = SongArrangementLabel::query()
|
||||
->where('song_arrangement_id', $clone->id)
|
||||
->orderBy('order')
|
||||
->pluck('song_group_id')
|
||||
->pluck('label_id')
|
||||
->all();
|
||||
|
||||
$this->assertSame($originalGroups, $cloneGroups);
|
||||
$this->assertSame($originalLabels, $cloneLabels);
|
||||
}
|
||||
|
||||
public function test_update_arrangement_reorders_and_persists_groups(): void
|
||||
|
|
@ -92,19 +92,19 @@ public function test_update_arrangement_reorders_and_persists_groups(): void
|
|||
|
||||
$response = $this->put(route('arrangements.update', $normal), [
|
||||
'groups' => [
|
||||
['song_group_id' => $chorus->id, 'order' => 1],
|
||||
['song_group_id' => $bridge->id, 'order' => 2],
|
||||
['song_group_id' => $verse->id, 'order' => 3],
|
||||
['song_group_id' => $chorus->id, 'order' => 4],
|
||||
['label_id' => $chorus->id, 'order' => 1],
|
||||
['label_id' => $bridge->id, 'order' => 2],
|
||||
['label_id' => $verse->id, 'order' => 3],
|
||||
['label_id' => $chorus->id, 'order' => 4],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
$updated = SongArrangementGroup::query()
|
||||
$updated = SongArrangementLabel::query()
|
||||
->where('song_arrangement_id', $normal->id)
|
||||
->orderBy('order')
|
||||
->pluck('song_group_id')
|
||||
->pluck('label_id')
|
||||
->all();
|
||||
|
||||
$this->assertSame([
|
||||
|
|
@ -136,23 +136,9 @@ private function createSongWithDefaultArrangement(): array
|
|||
{
|
||||
$song = Song::factory()->create();
|
||||
|
||||
$verse = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'name' => 'Verse 1',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$chorus = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'name' => 'Chorus',
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
$bridge = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'name' => 'Bridge',
|
||||
'order' => 3,
|
||||
]);
|
||||
$verse = Label::factory()->create(['name' => 'Verse 1']);
|
||||
$chorus = Label::factory()->create(['name' => 'Chorus']);
|
||||
$bridge = Label::factory()->create(['name' => 'Bridge']);
|
||||
|
||||
$normal = SongArrangement::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
|
|
@ -160,21 +146,21 @@ private function createSongWithDefaultArrangement(): array
|
|||
'is_default' => true,
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $normal->id,
|
||||
'song_group_id' => $verse->id,
|
||||
'label_id' => $verse->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $normal->id,
|
||||
'song_group_id' => $chorus->id,
|
||||
'label_id' => $chorus->id,
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $normal->id,
|
||||
'song_group_id' => $verse->id,
|
||||
'label_id' => $verse->id,
|
||||
'order' => 3,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceAgendaItem;
|
||||
use App\Models\ServiceSong;
|
||||
|
|
@ -35,15 +36,21 @@ private function createSongWithContent(string $title = 'Test Song', ?string $ccl
|
|||
'copyright_text' => 'Test Publisher',
|
||||
]);
|
||||
|
||||
$verse = $song->groups()->create(['name' => 'Verse 1', 'color' => '#2196F3', 'order' => 0]);
|
||||
$verse->slides()->create(['order' => 0, 'text_content' => 'First line of verse']);
|
||||
$verse = Label::firstOrCreate(
|
||||
['name' => 'Verse 1 - '.$title],
|
||||
['color' => '#2196F3'],
|
||||
);
|
||||
$verse->songSlides()->create(['order' => 0, 'text_content' => 'First line of verse']);
|
||||
|
||||
$chorus = $song->groups()->create(['name' => 'Chorus', 'color' => '#F44336', 'order' => 1]);
|
||||
$chorus->slides()->create(['order' => 0, 'text_content' => 'Chorus text']);
|
||||
$chorus = Label::firstOrCreate(
|
||||
['name' => 'Chorus - '.$title],
|
||||
['color' => '#F44336'],
|
||||
);
|
||||
$chorus->songSlides()->create(['order' => 0, 'text_content' => 'Chorus text']);
|
||||
|
||||
$arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
|
||||
$arrangement->arrangementGroups()->create(['song_group_id' => $verse->id, 'order' => 0]);
|
||||
$arrangement->arrangementGroups()->create(['song_group_id' => $chorus->id, 'order' => 1]);
|
||||
$arrangement->arrangementLabels()->create(['label_id' => $verse->id, 'order' => 0]);
|
||||
$arrangement->arrangementLabels()->create(['label_id' => $chorus->id, 'order' => 1]);
|
||||
|
||||
return $song;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
|
@ -22,17 +23,23 @@ private function createSongWithContent(): Song
|
|||
'publisher' => 'Test Publisher',
|
||||
]);
|
||||
|
||||
$verse = $song->groups()->create(['name' => 'Verse 1', 'color' => '#2196F3', 'order' => 0]);
|
||||
$verse->slides()->create(['order' => 0, 'text_content' => 'First line of verse']);
|
||||
$verse->slides()->create(['order' => 1, 'text_content' => 'Second line of verse']);
|
||||
$verse = Label::firstOrCreate(
|
||||
['name' => 'Verse 1 - Export Test Song'],
|
||||
['color' => '#2196F3'],
|
||||
);
|
||||
$verse->songSlides()->create(['order' => 0, 'text_content' => 'First line of verse']);
|
||||
$verse->songSlides()->create(['order' => 1, 'text_content' => 'Second line of verse']);
|
||||
|
||||
$chorus = $song->groups()->create(['name' => 'Chorus', 'color' => '#F44336', 'order' => 1]);
|
||||
$chorus->slides()->create(['order' => 0, 'text_content' => 'Chorus text']);
|
||||
$chorus = Label::firstOrCreate(
|
||||
['name' => 'Chorus - Export Test Song'],
|
||||
['color' => '#F44336'],
|
||||
);
|
||||
$chorus->songSlides()->create(['order' => 0, 'text_content' => 'Chorus text']);
|
||||
|
||||
$arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
|
||||
$arrangement->arrangementGroups()->create(['song_group_id' => $verse->id, 'order' => 0]);
|
||||
$arrangement->arrangementGroups()->create(['song_group_id' => $chorus->id, 'order' => 1]);
|
||||
$arrangement->arrangementGroups()->create(['song_group_id' => $verse->id, 'order' => 2]);
|
||||
$arrangement->arrangementLabels()->create(['label_id' => $verse->id, 'order' => 0]);
|
||||
$arrangement->arrangementLabels()->create(['label_id' => $chorus->id, 'order' => 1]);
|
||||
$arrangement->arrangementLabels()->create(['label_id' => $verse->id, 'order' => 2]);
|
||||
|
||||
return $song;
|
||||
}
|
||||
|
|
@ -82,7 +89,9 @@ public function test_download_pro_roundtrip_import_export(): void
|
|||
$song = Song::find($songId);
|
||||
|
||||
$this->assertNotNull($song);
|
||||
$this->assertGreaterThan(0, $song->groups()->count());
|
||||
|
||||
$labelCount = $song->arrangements()->withCount('arrangementLabels')->get()->sum('arrangement_labels_count');
|
||||
$this->assertGreaterThan(0, $labelCount);
|
||||
|
||||
$exportResponse = $this->actingAs($user)->get("/api/songs/{$songId}/download-pro");
|
||||
$exportResponse->assertOk();
|
||||
|
|
@ -92,7 +101,6 @@ public function test_download_pro_roundtrip_preserves_content(): void
|
|||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
// 1. Import the reference .pro file
|
||||
$sourcePath = base_path('tests/fixtures/propresenter/Test.pro');
|
||||
$file = new \Illuminate\Http\UploadedFile($sourcePath, 'Test.pro', 'application/octet-stream', null, true);
|
||||
|
||||
|
|
@ -100,43 +108,45 @@ public function test_download_pro_roundtrip_preserves_content(): void
|
|||
$importResponse->assertOk();
|
||||
|
||||
$songId = $importResponse->json('songs.0.id');
|
||||
$originalSong = Song::with(['groups.slides', 'arrangements.arrangementGroups.group'])->find($songId);
|
||||
$originalSong = Song::with(['arrangements.arrangementLabels.label.songSlides'])->find($songId);
|
||||
$this->assertNotNull($originalSong);
|
||||
|
||||
// Snapshot original data
|
||||
$originalGroups = $originalSong->groups->sortBy('order')->values();
|
||||
$defaultArr = $originalSong->arrangements->firstWhere('is_default', true) ?? $originalSong->arrangements->first();
|
||||
$this->assertNotNull($defaultArr);
|
||||
|
||||
$originalArrangementLabels = $defaultArr->arrangementLabels->sortBy('order')->values();
|
||||
$originalArrangements = $originalSong->arrangements;
|
||||
|
||||
// 2. Export as .pro
|
||||
$exportResponse = $this->actingAs($user)->get("/api/songs/{$songId}/download-pro");
|
||||
$exportResponse->assertOk();
|
||||
|
||||
// Save exported content to temp file — BinaryFileResponse delivers a real file
|
||||
$tempPath = sys_get_temp_dir().'/roundtrip-test-'.uniqid().'.pro';
|
||||
/** @var \Symfony\Component\HttpFoundation\BinaryFileResponse $baseResponse */
|
||||
$baseResponse = $exportResponse->baseResponse;
|
||||
copy($baseResponse->getFile()->getPathname(), $tempPath);
|
||||
|
||||
// 3. Re-import the exported file as a new song (different ccli to avoid upsert)
|
||||
// Use the ProPresenter parser directly to read and verify
|
||||
$reImported = \ProPresenter\Parser\ProFileReader::read($tempPath);
|
||||
@unlink($tempPath);
|
||||
|
||||
// 4. Assert song name
|
||||
$this->assertSame($originalSong->title, $reImported->getName());
|
||||
|
||||
// 5. Assert groups match (same names, same order)
|
||||
$reImportedGroups = $reImported->getGroups();
|
||||
$this->assertCount($originalGroups->count(), $reImportedGroups, 'Group count mismatch');
|
||||
|
||||
foreach ($originalGroups as $index => $originalGroup) {
|
||||
$uniqueOriginalLabels = $originalArrangementLabels
|
||||
->map(fn ($al) => $al->label)
|
||||
->filter()
|
||||
->unique('id')
|
||||
->values();
|
||||
|
||||
$this->assertCount($uniqueOriginalLabels->count(), $reImportedGroups, 'Group count mismatch');
|
||||
|
||||
foreach ($uniqueOriginalLabels as $index => $originalLabel) {
|
||||
$reImportedGroup = $reImportedGroups[$index];
|
||||
$this->assertSame($originalGroup->name, $reImportedGroup->getName(), "Group name mismatch at index {$index}");
|
||||
$this->assertSame($originalLabel->name, $reImportedGroup->getName(), "Group name mismatch at index {$index}");
|
||||
|
||||
// Assert slides within group
|
||||
$originalSlides = $originalGroup->slides->sortBy('order')->values();
|
||||
$originalSlides = $originalLabel->songSlides->sortBy('order')->values();
|
||||
$reImportedSlides = $reImported->getSlidesForGroup($reImportedGroup);
|
||||
$this->assertCount($originalSlides->count(), $reImportedSlides, "Slide count mismatch for group '{$originalGroup->name}'");
|
||||
$this->assertCount($originalSlides->count(), $reImportedSlides, "Slide count mismatch for group '{$originalLabel->name}'");
|
||||
|
||||
foreach ($originalSlides as $slideIndex => $originalSlide) {
|
||||
$reImportedSlide = $reImportedSlides[$slideIndex];
|
||||
|
|
@ -144,32 +154,30 @@ public function test_download_pro_roundtrip_preserves_content(): void
|
|||
$this->assertSame(
|
||||
$originalSlide->text_content,
|
||||
$reImportedSlide->getPlainText(),
|
||||
"Slide text mismatch for group '{$originalGroup->name}' slide {$slideIndex}"
|
||||
"Slide text mismatch for group '{$originalLabel->name}' slide {$slideIndex}"
|
||||
);
|
||||
|
||||
// Assert translation if present
|
||||
if ($originalSlide->text_content_translated) {
|
||||
$this->assertTrue($reImportedSlide->hasTranslation(), "Expected translation for group '{$originalGroup->name}' slide {$slideIndex}");
|
||||
$this->assertTrue($reImportedSlide->hasTranslation(), "Expected translation for group '{$originalLabel->name}' slide {$slideIndex}");
|
||||
$this->assertSame(
|
||||
$originalSlide->text_content_translated,
|
||||
$reImportedSlide->getTranslation()?->getPlainText(),
|
||||
"Translation text mismatch for group '{$originalGroup->name}' slide {$slideIndex}"
|
||||
"Translation text mismatch for group '{$originalLabel->name}' slide {$slideIndex}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Assert arrangements match (same names, same group order)
|
||||
$reImportedArrangements = $reImported->getArrangements();
|
||||
$this->assertCount($originalArrangements->count(), $reImportedArrangements, 'Arrangement count mismatch');
|
||||
|
||||
foreach ($originalArrangements as $index => $originalArrangement) {
|
||||
foreach ($originalArrangements as $originalArrangement) {
|
||||
$reImportedArrangement = $reImported->getArrangementByName($originalArrangement->name);
|
||||
$this->assertNotNull($reImportedArrangement, "Arrangement '{$originalArrangement->name}' not found in re-import");
|
||||
|
||||
$originalGroupNames = $originalArrangement->arrangementGroups
|
||||
$originalGroupNames = $originalArrangement->arrangementLabels
|
||||
->sortBy('order')
|
||||
->map(fn ($ag) => $ag->group?->name)
|
||||
->map(fn ($al) => $al->label?->name)
|
||||
->filter()
|
||||
->values()
|
||||
->toArray();
|
||||
|
|
@ -186,7 +194,6 @@ public function test_download_pro_roundtrip_preserves_content(): void
|
|||
);
|
||||
}
|
||||
|
||||
// 7. Assert CCLI metadata
|
||||
if ($originalSong->ccli_id) {
|
||||
$this->assertSame((int) $originalSong->ccli_id, $reImported->getCcliSongNumber());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,10 @@ public function test_import_pro_datei_erstellt_song_mit_gruppen_und_slides(): vo
|
|||
|
||||
$song = Song::where('title', 'Test')->first();
|
||||
$this->assertNotNull($song);
|
||||
$this->assertSame(4, $song->groups()->count());
|
||||
$this->assertSame(5, $song->groups()->withCount('slides')->get()->sum('slides_count'));
|
||||
|
||||
$this->assertSame(4, \App\Models\Label::count());
|
||||
$this->assertSame(5, \App\Models\SongSlide::count());
|
||||
|
||||
$this->assertSame(2, $song->arrangements()->count());
|
||||
$this->assertTrue($song->has_translation);
|
||||
}
|
||||
|
|
@ -64,9 +66,18 @@ public function test_import_pro_upsert_mit_ccli_dupliziert_nicht(): void
|
|||
'title' => 'Old Title',
|
||||
'ccli_id' => '999',
|
||||
]);
|
||||
$existingSong->groups()->create(['name' => 'Old Group', 'color' => '#FF0000', 'order' => 0]);
|
||||
|
||||
$this->assertSame(1, $existingSong->groups()->count());
|
||||
$arrangement = $existingSong->arrangements()->create([
|
||||
'name' => 'Normal',
|
||||
'is_default' => true,
|
||||
]);
|
||||
$oldLabel = \App\Models\Label::firstOrCreate(['name' => 'Old Group'], ['color' => '#FF0000']);
|
||||
$arrangement->arrangementLabels()->create([
|
||||
'label_id' => $oldLabel->id,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $arrangement->arrangementLabels()->count());
|
||||
|
||||
$existingSong->update(['ccli_id' => '999']);
|
||||
$this->assertSame(1, Song::count());
|
||||
|
|
@ -114,6 +125,6 @@ public function test_import_pro_erstellt_arrangement_gruppen(): void
|
|||
|
||||
$this->assertNotNull($normalArrangement);
|
||||
$this->assertTrue($normalArrangement->is_default);
|
||||
$this->assertSame(5, $normalArrangement->arrangementGroups()->count());
|
||||
$this->assertSame(5, $normalArrangement->arrangementLabels()->count());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceSong;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongArrangement;
|
||||
use App\Models\SongArrangementGroup;
|
||||
use App\Models\SongGroup;
|
||||
use App\Models\SongArrangementLabel;
|
||||
use App\Models\User;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Song CRUD API Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
});
|
||||
|
||||
// --- INDEX / LIST ---
|
||||
|
||||
test('songs index returns paginated list', function () {
|
||||
Song::factory()->count(3)->create();
|
||||
|
||||
|
|
@ -75,8 +67,6 @@
|
|||
$response->assertUnauthorized();
|
||||
});
|
||||
|
||||
// --- STORE / CREATE ---
|
||||
|
||||
test('store creates song with default groups and arrangement', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->postJson('/api/songs', [
|
||||
|
|
@ -91,16 +81,12 @@
|
|||
$song = Song::where('title', 'Neues Lied')->first();
|
||||
expect($song)->not->toBeNull();
|
||||
|
||||
// Default groups: Strophe 1, Refrain, Bridge
|
||||
expect($song->groups)->toHaveCount(3);
|
||||
expect($song->groups->pluck('name')->toArray())
|
||||
->toBe(['Strophe 1', 'Refrain', 'Bridge']);
|
||||
|
||||
// Default "Normal" arrangement
|
||||
$arrangement = $song->arrangements()->where('is_default', true)->first();
|
||||
expect($arrangement)->not->toBeNull();
|
||||
expect($arrangement->name)->toBe('Normal');
|
||||
expect($arrangement->arrangementGroups)->toHaveCount(3);
|
||||
expect($arrangement->arrangementLabels)->toHaveCount(3);
|
||||
expect($arrangement->arrangementLabels->sortBy('order')->pluck('label.name')->toArray())
|
||||
->toBe(['Strophe 1', 'Refrain', 'Bridge']);
|
||||
});
|
||||
|
||||
test('store validates required title', function () {
|
||||
|
|
@ -136,12 +122,15 @@
|
|||
expect(Song::where('title', 'Song ohne CCLI')->first()->ccli_id)->toBeNull();
|
||||
});
|
||||
|
||||
// --- SHOW ---
|
||||
|
||||
test('show returns song with groups slides and arrangements', function () {
|
||||
$song = Song::factory()->create();
|
||||
$group = SongGroup::factory()->create(['song_id' => $song->id, 'name' => 'Strophe 1']);
|
||||
SongArrangement::factory()->create(['song_id' => $song->id, 'name' => 'Normal', 'is_default' => true]);
|
||||
$label = Label::factory()->create(['name' => 'Strophe 1']);
|
||||
$arrangement = SongArrangement::factory()->create(['song_id' => $song->id, 'name' => 'Normal', 'is_default' => true]);
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->getJson("/api/songs/{$song->id}");
|
||||
|
|
@ -174,8 +163,6 @@
|
|||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// --- UPDATE ---
|
||||
|
||||
test('update modifies song metadata', function () {
|
||||
$song = Song::factory()->create(['title' => 'Old Title']);
|
||||
|
||||
|
|
@ -197,7 +184,6 @@
|
|||
$songA = Song::factory()->create(['ccli_id' => '111111']);
|
||||
$songB = Song::factory()->create(['ccli_id' => '222222']);
|
||||
|
||||
// Try setting songB's ccli_id to songA's
|
||||
$response = $this->actingAs($this->user)
|
||||
->putJson("/api/songs/{$songB->id}", [
|
||||
'title' => $songB->title,
|
||||
|
|
@ -220,8 +206,6 @@
|
|||
$response->assertOk();
|
||||
});
|
||||
|
||||
// --- DESTROY / SOFT DELETE ---
|
||||
|
||||
test('destroy soft-deletes a song', function () {
|
||||
$song = Song::factory()->create();
|
||||
|
||||
|
|
@ -242,8 +226,6 @@
|
|||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// --- LAST USED IN SERVICE ---
|
||||
|
||||
test('last_used_in_service returns correct date from service_songs', function () {
|
||||
$song = Song::factory()->create();
|
||||
$serviceOld = Service::factory()->create(['date' => '2025-06-01']);
|
||||
|
|
@ -275,26 +257,24 @@
|
|||
expect($response->json('data.last_used_in_service'))->toBeNull();
|
||||
});
|
||||
|
||||
// --- SONG SERVICE: DUPLICATE ARRANGEMENT ---
|
||||
|
||||
test('duplicate arrangement clones arrangement with groups', function () {
|
||||
$song = Song::factory()->create();
|
||||
$group1 = SongGroup::factory()->create(['song_id' => $song->id, 'order' => 1]);
|
||||
$group2 = SongGroup::factory()->create(['song_id' => $song->id, 'order' => 2]);
|
||||
$label1 = Label::factory()->create();
|
||||
$label2 = Label::factory()->create();
|
||||
|
||||
$arrangement = SongArrangement::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'name' => 'Original',
|
||||
'is_default' => true,
|
||||
]);
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $group1->id,
|
||||
'label_id' => $label1->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $group2->id,
|
||||
'label_id' => $label2->id,
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
|
|
@ -303,7 +283,7 @@
|
|||
|
||||
expect($clone->name)->toBe('Klone');
|
||||
expect($clone->is_default)->toBeFalse();
|
||||
expect($clone->arrangementGroups)->toHaveCount(2);
|
||||
expect($clone->arrangementGroups->pluck('song_group_id')->toArray())
|
||||
->toBe($arrangement->arrangementGroups->pluck('song_group_id')->toArray());
|
||||
expect($clone->arrangementLabels)->toHaveCount(2);
|
||||
expect($clone->arrangementLabels->pluck('label_id')->toArray())
|
||||
->toBe($arrangement->arrangementLabels->pluck('label_id')->toArray());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,28 +1,15 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongArrangement;
|
||||
use App\Models\SongArrangementGroup;
|
||||
use App\Models\SongGroup;
|
||||
use App\Models\SongArrangementLabel;
|
||||
use App\Models\User;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Song Edit Modal Tests
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Tests verifying the API endpoints used by SongEditModal.vue:
|
||||
| - GET /api/songs/{id} (show with groups + arrangements)
|
||||
| - PUT /api/songs/{id} (auto-save metadata)
|
||||
|
|
||||
*/
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
});
|
||||
|
||||
// --- Modal Data Loading ---
|
||||
|
||||
test('show returns song with full detail for modal', function () {
|
||||
$song = Song::factory()->create([
|
||||
'title' => 'Großer Gott wir loben Dich',
|
||||
|
|
@ -30,18 +17,14 @@
|
|||
'copyright_text' => '© Public Domain',
|
||||
]);
|
||||
|
||||
$group1 = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$label1 = Label::factory()->create([
|
||||
'name' => 'Strophe 1',
|
||||
'color' => '#3B82F6',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$group2 = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$label2 = Label::factory()->create([
|
||||
'name' => 'Refrain',
|
||||
'color' => '#10B981',
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
$arrangement = SongArrangement::factory()->create([
|
||||
|
|
@ -50,15 +33,15 @@
|
|||
'is_default' => true,
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $group1->id,
|
||||
'label_id' => $label1->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $group2->id,
|
||||
'label_id' => $label2->id,
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
|
|
@ -77,17 +60,13 @@
|
|||
],
|
||||
]);
|
||||
|
||||
// Groups in correct order
|
||||
expect($response->json('data.groups.0.name'))->toBe('Strophe 1');
|
||||
expect($response->json('data.groups.1.name'))->toBe('Refrain');
|
||||
|
||||
// Arrangement with arrangement_groups
|
||||
expect($response->json('data.arrangements.0.name'))->toBe('Normal');
|
||||
expect($response->json('data.arrangements.0.arrangement_groups'))->toHaveCount(2);
|
||||
});
|
||||
|
||||
// --- Metadata Auto-Save ---
|
||||
|
||||
test('update saves title via auto-save', function () {
|
||||
$song = Song::factory()->create(['title' => 'Original Title']);
|
||||
|
||||
|
|
@ -155,7 +134,6 @@
|
|||
|
||||
test('update returns full song detail with arrangements', function () {
|
||||
$song = Song::factory()->create();
|
||||
SongGroup::factory()->create(['song_id' => $song->id]);
|
||||
SongArrangement::factory()->create(['song_id' => $song->id, 'is_default' => true]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongArrangement;
|
||||
use App\Models\SongArrangementGroup;
|
||||
use App\Models\SongGroup;
|
||||
use App\Models\SongArrangementLabel;
|
||||
use App\Models\SongSlide;
|
||||
use App\Models\User;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Song PDF Download Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
});
|
||||
|
|
@ -24,22 +18,20 @@
|
|||
'name' => 'Normal',
|
||||
]);
|
||||
|
||||
$group = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$label = Label::factory()->create([
|
||||
'name' => 'Verse 1',
|
||||
'color' => '#3B82F6',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Amazing grace how sweet the sound',
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
|
|
@ -73,42 +65,37 @@
|
|||
'name' => 'Normal',
|
||||
]);
|
||||
|
||||
$verse = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$verse = Label::factory()->create([
|
||||
'name' => 'Strophe 1',
|
||||
'color' => '#3B82F6',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$chorus = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$chorus = Label::factory()->create([
|
||||
'name' => 'Refrain',
|
||||
'color' => '#10B981',
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $verse->id,
|
||||
'label_id' => $verse->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Großer Gott wir loben dich',
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $chorus->id,
|
||||
'label_id' => $chorus->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Heilig heilig heilig',
|
||||
]);
|
||||
|
||||
// Arrangement: Strophe 1 -> Refrain
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $verse->id,
|
||||
'label_id' => $verse->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $chorus->id,
|
||||
'label_id' => $chorus->id,
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
|
|
@ -130,22 +117,20 @@
|
|||
'name' => 'Normal',
|
||||
]);
|
||||
|
||||
$group = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$label = Label::factory()->create([
|
||||
'name' => 'Verse 1',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Amazing grace how sweet the sound',
|
||||
'text_content_translated' => 'Erstaunliche Gnade wie süß der Klang',
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
|
|
@ -210,21 +195,19 @@
|
|||
'name' => 'Übung',
|
||||
]);
|
||||
|
||||
$group = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$label = Label::factory()->create([
|
||||
'name' => 'Strophe 1',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Großer Gott wir loben dich',
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
|
|
@ -234,7 +217,6 @@
|
|||
$response->assertOk();
|
||||
$response->assertHeader('Content-Type', 'application/pdf');
|
||||
|
||||
// Filename should contain slug with umlauts handled
|
||||
$contentDisposition = $response->headers->get('Content-Disposition');
|
||||
expect($contentDisposition)->toContain('.pdf');
|
||||
});
|
||||
|
|
@ -260,28 +242,24 @@
|
|||
'ccli_id' => '123456',
|
||||
]);
|
||||
|
||||
$verse = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$verse = Label::factory()->create([
|
||||
'name' => 'Strophe 1',
|
||||
'color' => '#3b82f6',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$chorus = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$chorus = Label::factory()->create([
|
||||
'name' => 'Refrain',
|
||||
'color' => '#ef4444',
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $verse->id,
|
||||
'label_id' => $verse->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Strophe Text',
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $chorus->id,
|
||||
'label_id' => $chorus->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Refrain Text',
|
||||
]);
|
||||
|
|
@ -291,16 +269,15 @@
|
|||
'name' => 'Normal',
|
||||
]);
|
||||
|
||||
// Order: Chorus first, then Verse
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $chorus->id,
|
||||
'label_id' => $chorus->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $verse->id,
|
||||
'label_id' => $verse->id,
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
|
|
@ -321,7 +298,6 @@
|
|||
],
|
||||
]);
|
||||
|
||||
// Chorus should be first (order=1), Verse second (order=2)
|
||||
$data = $response->json();
|
||||
expect($data['groups'][0]['name'])->toBe('Refrain');
|
||||
expect($data['groups'][1]['name'])->toBe('Strophe 1');
|
||||
|
|
@ -331,14 +307,12 @@
|
|||
test('song preview includes translation text when slides have translations', function () {
|
||||
$song = Song::factory()->create(['title' => 'Lied mit Übersetzung']);
|
||||
|
||||
$group = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$label = Label::factory()->create([
|
||||
'name' => 'Verse',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Original Text',
|
||||
'text_content_translated' => 'Translated Text',
|
||||
|
|
@ -348,9 +322,9 @@
|
|||
'song_id' => $song->id,
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceSong;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongArrangement;
|
||||
use App\Models\SongArrangementGroup;
|
||||
use App\Models\SongGroup;
|
||||
use App\Models\SongArrangementLabel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
|
@ -79,18 +79,14 @@ public function test_songs_block_provides_matched_song_data_for_arrangement_conf
|
|||
'has_translation' => true,
|
||||
]);
|
||||
|
||||
$verse = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$verse = Label::factory()->create([
|
||||
'name' => 'Strophe 1',
|
||||
'color' => '#3B82F6',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$chorus = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$chorus = Label::factory()->create([
|
||||
'name' => 'Refrain',
|
||||
'color' => '#10B981',
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
$normal = SongArrangement::factory()->create([
|
||||
|
|
@ -99,15 +95,15 @@ public function test_songs_block_provides_matched_song_data_for_arrangement_conf
|
|||
'is_default' => true,
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $normal->id,
|
||||
'song_group_id' => $verse->id,
|
||||
'label_id' => $verse->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
SongArrangementGroup::factory()->create([
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $normal->id,
|
||||
'song_group_id' => $chorus->id,
|
||||
'label_id' => $chorus->id,
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Controllers\TranslationController;
|
||||
use App\Models\Label;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongGroup;
|
||||
use App\Models\SongArrangement;
|
||||
use App\Models\SongArrangementLabel;
|
||||
use App\Models\SongSlide;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
|
@ -21,36 +23,50 @@ public function test_translate_page_response_contains_ordered_groups_and_slides(
|
|||
'title' => 'Grosser Gott',
|
||||
]);
|
||||
|
||||
$groupLater = SongGroup::factory()->create([
|
||||
$arrangement = SongArrangement::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'name' => 'Refrain',
|
||||
'color' => '#22c55e',
|
||||
'order' => 2,
|
||||
'name' => 'Normal',
|
||||
'is_default' => true,
|
||||
]);
|
||||
|
||||
$groupFirst = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
$labelLater = Label::factory()->create([
|
||||
'name' => 'Refrain',
|
||||
'color' => '#22c55e',
|
||||
]);
|
||||
|
||||
$labelFirst = Label::factory()->create([
|
||||
'name' => 'Strophe 1',
|
||||
'color' => '#0ea5e9',
|
||||
]);
|
||||
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'label_id' => $labelFirst->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'label_id' => $labelLater->id,
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $groupFirst->id,
|
||||
'label_id' => $labelFirst->id,
|
||||
'order' => 2,
|
||||
'text_content' => "Zeile A\nZeile B",
|
||||
'text_content_translated' => "Line A\nLine B",
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $groupFirst->id,
|
||||
'label_id' => $labelFirst->id,
|
||||
'order' => 1,
|
||||
'text_content' => "Zeile C\nZeile D\nZeile E",
|
||||
'text_content_translated' => null,
|
||||
]);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $groupLater->id,
|
||||
'label_id' => $labelLater->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Refrain',
|
||||
'text_content_translated' => 'Chorus',
|
||||
|
|
|
|||
|
|
@ -1,25 +1,19 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Label;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongGroup;
|
||||
use App\Models\SongArrangement;
|
||||
use App\Models\SongArrangementLabel;
|
||||
use App\Models\SongSlide;
|
||||
use App\Models\User;
|
||||
use App\Services\TranslationService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Translation Service Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->service = app(TranslationService::class);
|
||||
});
|
||||
|
||||
// --- URL FETCH ---
|
||||
|
||||
test('fetchFromUrl returns text from successful HTTP response', function () {
|
||||
Http::fake([
|
||||
'https://example.com/lyrics' => Http::response('<html><body><p>Zeile 1</p><p>Zeile 2</p></body></html>', 200),
|
||||
|
|
@ -30,7 +24,6 @@
|
|||
expect($result)->not->toBeNull();
|
||||
expect($result)->toContain('Zeile 1');
|
||||
expect($result)->toContain('Zeile 2');
|
||||
// HTML tags should be stripped
|
||||
expect($result)->not->toContain('<p>');
|
||||
expect($result)->not->toContain('<html>');
|
||||
});
|
||||
|
|
@ -65,34 +58,56 @@
|
|||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
// --- IMPORT TRANSLATION (LINE-COUNT DISTRIBUTION) ---
|
||||
|
||||
test('importTranslation distributes lines by slide line counts', function () {
|
||||
function makeSongWithDefaultArrangement(): array
|
||||
{
|
||||
$song = Song::factory()->create(['has_translation' => false]);
|
||||
|
||||
$group = SongGroup::factory()->create([
|
||||
$arrangement = SongArrangement::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'name' => 'Strophe 1',
|
||||
'order' => 1,
|
||||
'name' => 'Normal',
|
||||
'is_default' => true,
|
||||
]);
|
||||
|
||||
// Slide 1: 4 lines
|
||||
return [$song, $arrangement];
|
||||
}
|
||||
|
||||
function attachLabelWithSlides(SongArrangement $arrangement, string $labelName, array $slides, int $arrangementOrder): Label
|
||||
{
|
||||
$label = Label::firstOrCreate(['name' => $labelName]);
|
||||
SongArrangementLabel::factory()->create([
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => $arrangementOrder,
|
||||
]);
|
||||
|
||||
foreach ($slides as $slide) {
|
||||
SongSlide::factory()->create(array_merge(
|
||||
['label_id' => $label->id],
|
||||
$slide,
|
||||
));
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
test('importTranslation distributes lines by slide line counts', function () {
|
||||
[$song, $arrangement] = makeSongWithDefaultArrangement();
|
||||
|
||||
$label = attachLabelWithSlides($arrangement, 'Strophe 1 - dist', [], 1);
|
||||
|
||||
$slide1 = SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
'text_content' => "Original 1\nOriginal 2\nOriginal 3\nOriginal 4",
|
||||
]);
|
||||
|
||||
// Slide 2: 2 lines
|
||||
$slide2 = SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 2,
|
||||
'text_content' => "Original 5\nOriginal 6",
|
||||
]);
|
||||
|
||||
// Slide 3: 4 lines
|
||||
$slide3 = SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 3,
|
||||
'text_content' => "Original 7\nOriginal 8\nOriginal 9\nOriginal 10",
|
||||
]);
|
||||
|
|
@ -105,37 +120,25 @@
|
|||
$slide2->refresh();
|
||||
$slide3->refresh();
|
||||
|
||||
// Slide 1 gets lines 1-4
|
||||
expect($slide1->text_content_translated)->toBe("Zeile 1\nZeile 2\nZeile 3\nZeile 4");
|
||||
// Slide 2 gets lines 5-6
|
||||
expect($slide2->text_content_translated)->toBe("Zeile 5\nZeile 6");
|
||||
// Slide 3 gets lines 7-10
|
||||
expect($slide3->text_content_translated)->toBe("Zeile 7\nZeile 8\nZeile 9\nZeile 10");
|
||||
});
|
||||
|
||||
test('importTranslation distributes across multiple groups', function () {
|
||||
$song = Song::factory()->create(['has_translation' => false]);
|
||||
[$song, $arrangement] = makeSongWithDefaultArrangement();
|
||||
|
||||
$group1 = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'name' => 'Strophe 1',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$group2 = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'name' => 'Refrain',
|
||||
'order' => 2,
|
||||
]);
|
||||
$label1 = attachLabelWithSlides($arrangement, 'Strophe 1 - multi', [], 1);
|
||||
$label2 = attachLabelWithSlides($arrangement, 'Refrain - multi', [], 2);
|
||||
|
||||
$slide1 = SongSlide::factory()->create([
|
||||
'song_group_id' => $group1->id,
|
||||
'label_id' => $label1->id,
|
||||
'order' => 1,
|
||||
'text_content' => "Line A\nLine B",
|
||||
]);
|
||||
|
||||
$slide2 = SongSlide::factory()->create([
|
||||
'song_group_id' => $group2->id,
|
||||
'label_id' => $label2->id,
|
||||
'order' => 1,
|
||||
'text_content' => "Line C\nLine D\nLine E",
|
||||
]);
|
||||
|
|
@ -152,26 +155,22 @@
|
|||
});
|
||||
|
||||
test('importTranslation handles fewer translation lines than original', function () {
|
||||
$song = Song::factory()->create(['has_translation' => false]);
|
||||
[$song, $arrangement] = makeSongWithDefaultArrangement();
|
||||
|
||||
$group = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
$label = attachLabelWithSlides($arrangement, 'Strophe 1 - fewer', [], 1);
|
||||
|
||||
$slide1 = SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
'text_content' => "Line 1\nLine 2\nLine 3",
|
||||
]);
|
||||
|
||||
$slide2 = SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 2,
|
||||
'text_content' => "Line 4\nLine 5",
|
||||
]);
|
||||
|
||||
// Only 3 lines for 5 lines total
|
||||
$translatedText = "Zeile 1\nZeile 2\nZeile 3";
|
||||
|
||||
$this->service->importTranslation($song, $translatedText);
|
||||
|
|
@ -179,22 +178,16 @@
|
|||
$slide1->refresh();
|
||||
$slide2->refresh();
|
||||
|
||||
// Slide 1 gets all 3 available lines
|
||||
expect($slide1->text_content_translated)->toBe("Zeile 1\nZeile 2\nZeile 3");
|
||||
// Slide 2 gets empty (no lines left)
|
||||
expect($slide2->text_content_translated)->toBe('');
|
||||
});
|
||||
|
||||
test('importTranslation marks song as translated', function () {
|
||||
$song = Song::factory()->create(['has_translation' => false]);
|
||||
|
||||
$group = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
[$song, $arrangement] = makeSongWithDefaultArrangement();
|
||||
|
||||
$label = attachLabelWithSlides($arrangement, 'Strophe 1 - mark', [], 1);
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Line 1',
|
||||
]);
|
||||
|
|
@ -205,8 +198,6 @@
|
|||
expect($song->has_translation)->toBeTrue();
|
||||
});
|
||||
|
||||
// --- MARK AS TRANSLATED ---
|
||||
|
||||
test('markAsTranslated sets has_translation to true', function () {
|
||||
$song = Song::factory()->create(['has_translation' => false]);
|
||||
|
||||
|
|
@ -216,25 +207,21 @@
|
|||
expect($song->has_translation)->toBeTrue();
|
||||
});
|
||||
|
||||
// --- REMOVE TRANSLATION ---
|
||||
|
||||
test('removeTranslation clears all translated text and sets flag to false', function () {
|
||||
$song = Song::factory()->create(['has_translation' => true]);
|
||||
[$song, $arrangement] = makeSongWithDefaultArrangement();
|
||||
$song->update(['has_translation' => true]);
|
||||
|
||||
$group = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
$label = attachLabelWithSlides($arrangement, 'Strophe 1 - remove', [], 1);
|
||||
|
||||
$slide1 = SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Original',
|
||||
'text_content_translated' => 'Übersetzt',
|
||||
]);
|
||||
|
||||
$slide2 = SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 2,
|
||||
'text_content' => 'Original 2',
|
||||
'text_content_translated' => 'Übersetzt 2',
|
||||
|
|
@ -251,8 +238,6 @@
|
|||
expect($slide2->text_content_translated)->toBeNull();
|
||||
});
|
||||
|
||||
// --- CONTROLLER ENDPOINTS ---
|
||||
|
||||
test('POST translation/fetch-url returns scraped text', function () {
|
||||
Http::fake([
|
||||
'https://lyrics.example.com/song' => Http::response('<div>Liedtext Zeile 1</div>', 200),
|
||||
|
|
@ -292,15 +277,12 @@
|
|||
});
|
||||
|
||||
test('POST songs/{song}/translation/import distributes and saves translation', function () {
|
||||
$song = Song::factory()->create(['has_translation' => false]);
|
||||
[$song, $arrangement] = makeSongWithDefaultArrangement();
|
||||
|
||||
$group = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
$label = attachLabelWithSlides($arrangement, 'Strophe 1 - controller', [], 1);
|
||||
|
||||
$slide = SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
'text_content' => "Line 1\nLine 2",
|
||||
]);
|
||||
|
|
@ -340,15 +322,13 @@
|
|||
});
|
||||
|
||||
test('DELETE songs/{song}/translation removes translation', function () {
|
||||
$song = Song::factory()->create(['has_translation' => true]);
|
||||
[$song, $arrangement] = makeSongWithDefaultArrangement();
|
||||
$song->update(['has_translation' => true]);
|
||||
|
||||
$group = SongGroup::factory()->create([
|
||||
'song_id' => $song->id,
|
||||
'order' => 1,
|
||||
]);
|
||||
$label = attachLabelWithSlides($arrangement, 'Strophe 1 - delete', [], 1);
|
||||
|
||||
SongSlide::factory()->create([
|
||||
'song_group_id' => $group->id,
|
||||
'label_id' => $label->id,
|
||||
'order' => 1,
|
||||
'text_content' => 'Original',
|
||||
'text_content_translated' => 'Übersetzt',
|
||||
|
|
|
|||
6
tests/fixtures/generate-labels-sample.php
vendored
6
tests/fixtures/generate-labels-sample.php
vendored
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
function makeColor(float $r, float $g, float $b, float $a = 1.0): Color
|
||||
{
|
||||
$color = new Color();
|
||||
$color = new Color;
|
||||
$color->setRed($r);
|
||||
$color->setGreen($g);
|
||||
$color->setBlue($b);
|
||||
|
|
@ -17,7 +17,7 @@ function makeColor(float $r, float $g, float $b, float $a = 1.0): Color
|
|||
return $color;
|
||||
}
|
||||
|
||||
$doc = new ProLabelsDocument();
|
||||
$doc = new ProLabelsDocument;
|
||||
|
||||
$labels = [
|
||||
['name' => 'Copyright', 'r' => 0.8, 'g' => 0.2, 'b' => 0.2],
|
||||
|
|
@ -28,7 +28,7 @@ function makeColor(float $r, float $g, float $b, float $a = 1.0): Color
|
|||
|
||||
$labelObjects = [];
|
||||
foreach ($labels as $data) {
|
||||
$label = new Label();
|
||||
$label = new Label;
|
||||
$label->setText($data['name']);
|
||||
$label->setColor(makeColor($data['r'], $data['g'], $data['b']));
|
||||
$labelObjects[] = $label;
|
||||
|
|
|
|||
12
tests/fixtures/generate-macros-sample.php
vendored
12
tests/fixtures/generate-macros-sample.php
vendored
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
function makeUuid(string $value): UUID
|
||||
{
|
||||
$uuid = new UUID();
|
||||
$uuid = new UUID;
|
||||
$uuid->setString(strtoupper($value));
|
||||
|
||||
return $uuid;
|
||||
|
|
@ -19,7 +19,7 @@ function makeUuid(string $value): UUID
|
|||
|
||||
function makeColor(float $r, float $g, float $b, float $a = 1.0): Color
|
||||
{
|
||||
$color = new Color();
|
||||
$color = new Color;
|
||||
$color->setRed($r);
|
||||
$color->setGreen($g);
|
||||
$color->setBlue($b);
|
||||
|
|
@ -28,7 +28,7 @@ function makeColor(float $r, float $g, float $b, float $a = 1.0): Color
|
|||
return $color;
|
||||
}
|
||||
|
||||
$doc = new MacrosDocument();
|
||||
$doc = new MacrosDocument;
|
||||
|
||||
$macros = [
|
||||
['uuid' => 'AAAAAAAA-1111-2222-3333-FFFFFFFFFFFF', 'name' => 'Copyright Makro', 'r' => 1.0, 'g' => 0.5, 'b' => 0.0],
|
||||
|
|
@ -38,7 +38,7 @@ function makeColor(float $r, float $g, float $b, float $a = 1.0): Color
|
|||
|
||||
$macroObjects = [];
|
||||
foreach ($macros as $data) {
|
||||
$macro = new Macro();
|
||||
$macro = new Macro;
|
||||
$macro->setUuid(makeUuid($data['uuid']));
|
||||
$macro->setName($data['name']);
|
||||
$macro->setColor(makeColor($data['r'], $data['g'], $data['b']));
|
||||
|
|
@ -48,13 +48,13 @@ function makeColor(float $r, float $g, float $b, float $a = 1.0): Color
|
|||
}
|
||||
$doc->setMacros($macroObjects);
|
||||
|
||||
$collection = new MacroCollection();
|
||||
$collection = new MacroCollection;
|
||||
$collection->setUuid(makeUuid('8D02FC57-83F8-4042-9B90-81C229728426'));
|
||||
$collection->setName('--MAIN--');
|
||||
|
||||
$items = [];
|
||||
foreach ($macros as $data) {
|
||||
$item = new Item();
|
||||
$item = new Item;
|
||||
$item->setMacroId(makeUuid($data['uuid']));
|
||||
$items[] = $item;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue