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\Services\ProBundleExportService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
|
|
@ -275,7 +275,7 @@ public function download(Service $service): JsonResponse|BinaryFileResponse
|
|||
}
|
||||
|
||||
try {
|
||||
$playlistService = new \App\Services\PlaylistExportService();
|
||||
$playlistService = new \App\Services\PlaylistExportService;
|
||||
$result = $playlistService->generatePlaylist($service);
|
||||
|
||||
$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 = [
|
||||
'type',
|
||||
'service_id',
|
||||
'service_agenda_item_id',
|
||||
'original_filename',
|
||||
'stored_filename',
|
||||
'thumbnail_filename',
|
||||
|
|
@ -36,4 +37,9 @@ public function service(): BelongsTo
|
|||
{
|
||||
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();
|
||||
$skippedEmpty = 0;
|
||||
|
||||
$exportService = new ProExportService();
|
||||
$exportService = new ProExportService;
|
||||
$tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid();
|
||||
mkdir($tempDir, 0755, true);
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ public function generateBundle(Service $service, string $blockType): string
|
|||
|
||||
$bundlePath = sys_get_temp_dir().'/'.uniqid($blockType.'-').'.probundle';
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$zip = new ZipArchive;
|
||||
$openResult = $zip->open($bundlePath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
||||
if ($openResult !== true) {
|
||||
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';
|
||||
copy($baseResponse->getFile()->getPathname(), $copiedPath);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$zip = new ZipArchive;
|
||||
$openResult = $zip->open($copiedPath);
|
||||
|
||||
$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';
|
||||
copy($baseResponse->getFile()->getPathname(), $copiedPath);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$zip = new ZipArchive;
|
||||
$openResult = $zip->open($copiedPath);
|
||||
|
||||
$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