feat(db): add service_agenda_items table + slides FK migration
This commit is contained in:
parent
5cf0c43241
commit
31d7634dbf
|
|
@ -7,8 +7,8 @@
|
||||||
use App\Models\Song;
|
use App\Models\Song;
|
||||||
use App\Services\ProBundleExportService;
|
use App\Services\ProBundleExportService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
@ -275,7 +275,7 @@ public function download(Service $service): JsonResponse|BinaryFileResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$playlistService = new \App\Services\PlaylistExportService();
|
$playlistService = new \App\Services\PlaylistExportService;
|
||||||
$result = $playlistService->generatePlaylist($service);
|
$result = $playlistService->generatePlaylist($service);
|
||||||
|
|
||||||
$response = response()->download($result['path'], $result['filename']);
|
$response = response()->download($result['path'], $result['filename']);
|
||||||
|
|
|
||||||
57
app/Models/ServiceAgendaItem.php
Normal file
57
app/Models/ServiceAgendaItem.php
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class ServiceAgendaItem extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'service_id',
|
||||||
|
'cts_agenda_item_id',
|
||||||
|
'position',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
'note',
|
||||||
|
'duration',
|
||||||
|
'start',
|
||||||
|
'is_before_event',
|
||||||
|
'responsible',
|
||||||
|
'service_song_id',
|
||||||
|
'sort_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'responsible' => 'array',
|
||||||
|
'is_before_event' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function service(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Service::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serviceSong(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ServiceSong::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function slides(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Slide::class, 'service_agenda_item_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeVisible(Builder $query): void
|
||||||
|
{
|
||||||
|
$query->where('is_before_event', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ class Slide extends Model
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'type',
|
'type',
|
||||||
'service_id',
|
'service_id',
|
||||||
|
'service_agenda_item_id',
|
||||||
'original_filename',
|
'original_filename',
|
||||||
'stored_filename',
|
'stored_filename',
|
||||||
'thumbnail_filename',
|
'thumbnail_filename',
|
||||||
|
|
@ -36,4 +37,9 @@ public function service(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Service::class);
|
return $this->belongsTo(Service::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function serviceAgendaItem(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ServiceAgendaItem::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ public function generatePlaylist(Service $service): array
|
||||||
$skippedUnmatched = $service->serviceSongs()->whereNull('song_id')->count();
|
$skippedUnmatched = $service->serviceSongs()->whereNull('song_id')->count();
|
||||||
$skippedEmpty = 0;
|
$skippedEmpty = 0;
|
||||||
|
|
||||||
$exportService = new ProExportService();
|
$exportService = new ProExportService;
|
||||||
$tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid();
|
$tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid();
|
||||||
mkdir($tempDir, 0755, true);
|
mkdir($tempDir, 0755, true);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ public function generateBundle(Service $service, string $blockType): string
|
||||||
|
|
||||||
$bundlePath = sys_get_temp_dir().'/'.uniqid($blockType.'-').'.probundle';
|
$bundlePath = sys_get_temp_dir().'/'.uniqid($blockType.'-').'.probundle';
|
||||||
|
|
||||||
$zip = new ZipArchive();
|
$zip = new ZipArchive;
|
||||||
$openResult = $zip->open($bundlePath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
$openResult = $zip->open($bundlePath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
||||||
if ($openResult !== true) {
|
if ($openResult !== true) {
|
||||||
throw new InvalidArgumentException('Konnte .probundle nicht erstellen.');
|
throw new InvalidArgumentException('Konnte .probundle nicht erstellen.');
|
||||||
|
|
|
||||||
51
database/factories/ServiceAgendaItemFactory.php
Normal file
51
database/factories/ServiceAgendaItemFactory.php
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\ServiceAgendaItem;
|
||||||
|
use App\Models\ServiceSong;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class ServiceAgendaItemFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = ServiceAgendaItem::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'service_id' => Service::factory(),
|
||||||
|
'cts_agenda_item_id' => $this->faker->uuid(),
|
||||||
|
'position' => $this->faker->numerify('#.#'),
|
||||||
|
'title' => $this->faker->sentence(3),
|
||||||
|
'type' => $this->faker->randomElement(['Song', 'Default', 'Header']),
|
||||||
|
'note' => $this->faker->optional()->sentence(),
|
||||||
|
'duration' => $this->faker->optional()->numerify('#'),
|
||||||
|
'start' => $this->faker->optional()->time(),
|
||||||
|
'is_before_event' => false,
|
||||||
|
'responsible' => $this->faker->optional()->randomElements(
|
||||||
|
['person1', 'person2', 'person3'],
|
||||||
|
$this->faker->numberBetween(1, 2)
|
||||||
|
),
|
||||||
|
'service_song_id' => null,
|
||||||
|
'sort_order' => $this->faker->numberBetween(1, 20),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withSong(ServiceSong $serviceSong): self
|
||||||
|
{
|
||||||
|
return $this->state(fn () => [
|
||||||
|
'service_song_id' => $serviceSong->id,
|
||||||
|
'service_id' => $serviceSong->service_id,
|
||||||
|
'type' => 'Song',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nonSong(): self
|
||||||
|
{
|
||||||
|
return $this->state(fn () => [
|
||||||
|
'service_song_id' => null,
|
||||||
|
'type' => $this->faker->randomElement(['Default', 'Header']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('service_agenda_items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('service_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('cts_agenda_item_id')->nullable()->index();
|
||||||
|
$table->string('position');
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('type');
|
||||||
|
$table->text('note')->nullable();
|
||||||
|
$table->string('duration')->nullable();
|
||||||
|
$table->string('start')->nullable();
|
||||||
|
$table->boolean('is_before_event')->default(false);
|
||||||
|
$table->json('responsible')->nullable();
|
||||||
|
$table->foreignId('service_song_id')->nullable()->constrained()->nullOnDelete();
|
||||||
|
$table->unsignedInteger('sort_order');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('service_id');
|
||||||
|
$table->unique(['service_id', 'sort_order']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('service_agenda_items');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('slides', function (Blueprint $table) {
|
||||||
|
$table->foreignId('service_agenda_item_id')->nullable()->after('service_id')->constrained()->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('slides', function (Blueprint $table) {
|
||||||
|
$table->dropConstrainedForeignId('service_agenda_item_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -60,7 +60,7 @@ public function test_probundle_enthaelt_data_und_bilder(): void
|
||||||
$copiedPath = sys_get_temp_dir().'/probundle-test-'.uniqid().'.probundle';
|
$copiedPath = sys_get_temp_dir().'/probundle-test-'.uniqid().'.probundle';
|
||||||
copy($baseResponse->getFile()->getPathname(), $copiedPath);
|
copy($baseResponse->getFile()->getPathname(), $copiedPath);
|
||||||
|
|
||||||
$zip = new ZipArchive();
|
$zip = new ZipArchive;
|
||||||
$openResult = $zip->open($copiedPath);
|
$openResult = $zip->open($copiedPath);
|
||||||
|
|
||||||
$this->assertTrue($openResult === true);
|
$this->assertTrue($openResult === true);
|
||||||
|
|
@ -128,7 +128,7 @@ public function test_probundle_ohne_slides_enthaelt_nur_data(): void
|
||||||
$copiedPath = sys_get_temp_dir().'/probundle-empty-test-'.uniqid().'.probundle';
|
$copiedPath = sys_get_temp_dir().'/probundle-empty-test-'.uniqid().'.probundle';
|
||||||
copy($baseResponse->getFile()->getPathname(), $copiedPath);
|
copy($baseResponse->getFile()->getPathname(), $copiedPath);
|
||||||
|
|
||||||
$zip = new ZipArchive();
|
$zip = new ZipArchive;
|
||||||
$openResult = $zip->open($copiedPath);
|
$openResult = $zip->open($copiedPath);
|
||||||
|
|
||||||
$this->assertTrue($openResult === true);
|
$this->assertTrue($openResult === true);
|
||||||
|
|
|
||||||
161
tests/Feature/ServiceAgendaItemTest.php
Normal file
161
tests/Feature/ServiceAgendaItemTest.php
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\ServiceAgendaItem;
|
||||||
|
use App\Models\ServiceSong;
|
||||||
|
use App\Models\Slide;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| ServiceAgendaItem Model & Migration Tests
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
test('migration creates service_agenda_items table with correct columns', function () {
|
||||||
|
expect(Schema::hasTable('service_agenda_items'))->toBeTrue();
|
||||||
|
|
||||||
|
$columns = [
|
||||||
|
'id',
|
||||||
|
'service_id',
|
||||||
|
'cts_agenda_item_id',
|
||||||
|
'position',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
'note',
|
||||||
|
'duration',
|
||||||
|
'start',
|
||||||
|
'is_before_event',
|
||||||
|
'responsible',
|
||||||
|
'service_song_id',
|
||||||
|
'sort_order',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
expect(Schema::hasColumn('service_agenda_items', $column))->toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('factory creates valid service agenda item', function () {
|
||||||
|
$item = ServiceAgendaItem::factory()->create();
|
||||||
|
|
||||||
|
expect($item)->toBeInstanceOf(ServiceAgendaItem::class);
|
||||||
|
expect($item->id)->toBeGreaterThan(0);
|
||||||
|
expect($item->service_id)->toBeGreaterThan(0);
|
||||||
|
expect($item->title)->not->toBeNull();
|
||||||
|
expect($item->type)->toBeIn(['Song', 'Default', 'Header']);
|
||||||
|
expect($item->position)->not->toBeNull();
|
||||||
|
expect($item->is_before_event)->toBeFalse();
|
||||||
|
expect($item->sort_order)->toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('service relationship returns correct service', function () {
|
||||||
|
$service = Service::factory()->create();
|
||||||
|
$item = ServiceAgendaItem::factory()->create(['service_id' => $service->id]);
|
||||||
|
|
||||||
|
expect($item->service)->toBeInstanceOf(Service::class);
|
||||||
|
expect($item->service->id)->toBe($service->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serviceSong relationship is nullable', function () {
|
||||||
|
$item = ServiceAgendaItem::factory()->create(['service_song_id' => null]);
|
||||||
|
|
||||||
|
expect($item->serviceSong)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serviceSong relationship returns correct service song', function () {
|
||||||
|
$song = ServiceSong::factory()->create();
|
||||||
|
$item = ServiceAgendaItem::factory()->create(['service_song_id' => $song->id]);
|
||||||
|
|
||||||
|
expect($item->serviceSong)->toBeInstanceOf(ServiceSong::class);
|
||||||
|
expect($item->serviceSong->id)->toBe($song->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('slides relationship returns associated slides', function () {
|
||||||
|
$item = ServiceAgendaItem::factory()->create();
|
||||||
|
Slide::factory()->count(3)->create(['service_agenda_item_id' => $item->id]);
|
||||||
|
|
||||||
|
expect($item->slides)->toHaveCount(3);
|
||||||
|
expect($item->slides->first())->toBeInstanceOf(Slide::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('scopeVisible excludes is_before_event true items', function () {
|
||||||
|
ServiceAgendaItem::factory()->count(2)->create(['is_before_event' => false]);
|
||||||
|
ServiceAgendaItem::factory()->count(3)->create(['is_before_event' => true]);
|
||||||
|
|
||||||
|
$visible = ServiceAgendaItem::visible()->get();
|
||||||
|
|
||||||
|
expect($visible)->toHaveCount(2);
|
||||||
|
$visible->each(fn ($item) => expect($item->is_before_event)->toBeFalse());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('responsible is cast to array', function () {
|
||||||
|
$item = ServiceAgendaItem::factory()->create([
|
||||||
|
'responsible' => ['person1', 'person2'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($item->responsible)->toBeArray();
|
||||||
|
expect($item->responsible)->toHaveCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is_before_event is cast to boolean', function () {
|
||||||
|
$item = ServiceAgendaItem::factory()->create(['is_before_event' => true]);
|
||||||
|
|
||||||
|
expect($item->is_before_event)->toBeTrue();
|
||||||
|
expect(is_bool($item->is_before_event))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unique constraint on service_id and sort_order', function () {
|
||||||
|
$service = Service::factory()->create();
|
||||||
|
ServiceAgendaItem::factory()->create([
|
||||||
|
'service_id' => $service->id,
|
||||||
|
'sort_order' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(fn () => ServiceAgendaItem::factory()->create([
|
||||||
|
'service_id' => $service->id,
|
||||||
|
'sort_order' => 1,
|
||||||
|
]))->toThrow(QueryException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withSong factory state', function () {
|
||||||
|
$song = ServiceSong::factory()->create();
|
||||||
|
$item = ServiceAgendaItem::factory()->withSong($song)->create();
|
||||||
|
|
||||||
|
expect($item->service_song_id)->toBe($song->id);
|
||||||
|
expect($item->service_id)->toBe($song->service_id);
|
||||||
|
expect($item->type)->toBe('Song');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nonSong factory state', function () {
|
||||||
|
$item = ServiceAgendaItem::factory()->nonSong()->create();
|
||||||
|
|
||||||
|
expect($item->service_song_id)->toBeNull();
|
||||||
|
expect($item->type)->toBeIn(['Default', 'Header']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cascadeOnDelete removes agenda items when service deleted', function () {
|
||||||
|
$service = Service::factory()->create();
|
||||||
|
ServiceAgendaItem::factory()->count(3)->create(['service_id' => $service->id]);
|
||||||
|
|
||||||
|
expect(ServiceAgendaItem::where('service_id', $service->id)->count())->toBe(3);
|
||||||
|
|
||||||
|
$service->delete();
|
||||||
|
|
||||||
|
expect(ServiceAgendaItem::where('service_id', $service->id)->count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullOnDelete nullifies service_song_id when song deleted', function () {
|
||||||
|
$song = ServiceSong::factory()->create();
|
||||||
|
$item = ServiceAgendaItem::factory()->create(['service_song_id' => $song->id]);
|
||||||
|
|
||||||
|
expect($item->service_song_id)->toBe($song->id);
|
||||||
|
|
||||||
|
$song->delete();
|
||||||
|
$item->refresh();
|
||||||
|
|
||||||
|
expect($item->service_song_id)->toBeNull();
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue