images: press-release-images:regenerate-variants --only=og für den Bestand, Testhelfer eindeutig benannt (Bildkennzeichnung H.1)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
62723b551a
commit
3b613fadf4
3 changed files with 135 additions and 5 deletions
101
app/Console/Commands/RegeneratePressReleaseImageVariants.php
Normal file
101
app/Console/Commands/RegeneratePressReleaseImageVariants.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Services\Image\ImageService;
|
||||
use App\Support\ImageCredit;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Bildkennzeichnung H.1, Bestand: OG-Variante mit eingebranntem Nachweis
|
||||
* für Bilder nachziehen, die noch keine haben (vor Formular v2 hochgeladen
|
||||
* oder importiert). Quelle ist die größte vorhandene Variante; der Nachweis
|
||||
* kommt aus der gemeinsamen Auflösung (Foto: … / Quelle: [Firma]).
|
||||
*/
|
||||
class RegeneratePressReleaseImageVariants extends Command
|
||||
{
|
||||
protected $signature = 'press-release-images:regenerate-variants
|
||||
{--only=og : Nur diese Variante (derzeit: og)}
|
||||
{--published : Nur Bilder veröffentlichter Meldungen}
|
||||
{--force : Auch vorhandene Varianten neu erzeugen}
|
||||
{--limit=0 : Höchstens so viele Bilder (0 = alle)}
|
||||
{--execute : Wirklich schreiben (Standard ist ein Trockenlauf)}';
|
||||
|
||||
protected $description = 'Erzeugt fehlende OG-Varianten mit eingebranntem Bildnachweis für den Bestand';
|
||||
|
||||
public function __construct(private readonly ImageService $imageService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if ($this->option('only') !== 'og') {
|
||||
$this->error('Derzeit wird nur --only=og unterstützt.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$query = PressReleaseImage::query()
|
||||
->with(['pressRelease' => fn ($q) => $q->withoutGlobalScope(PortalScope::class)->with('company')])
|
||||
->orderBy('id');
|
||||
|
||||
if ($this->option('published')) {
|
||||
$query->whereHas('pressRelease', fn ($q) => $q->withoutGlobalScope(PortalScope::class)->where('status', 'published'));
|
||||
}
|
||||
|
||||
if (! $this->option('force')) {
|
||||
$query->where(function ($q): void {
|
||||
$q->whereNull('variants')->orWhereNull('variants->og');
|
||||
});
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$limit = (int) $this->option('limit');
|
||||
$execute = (bool) $this->option('execute');
|
||||
|
||||
$this->info(($execute ? 'Erzeuge' : 'Trockenlauf:').' OG-Varianten für '.($limit > 0 ? min($limit, $total) : $total).' von '.$total.' Bildern.');
|
||||
|
||||
if (! $execute) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$done = 0;
|
||||
$skipped = 0;
|
||||
|
||||
$query->chunkById(200, function ($images) use (&$done, &$skipped, $limit): bool {
|
||||
foreach ($images as $image) {
|
||||
if ($limit > 0 && $done >= $limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$caption = $image->pressRelease ? ImageCredit::for($image, $image->pressRelease->company)->line() : null;
|
||||
$path = $this->imageService->regenerateOgVariant(
|
||||
$image->disk,
|
||||
$image->variants,
|
||||
$image->path,
|
||||
$image->focus_x !== null && $image->focus_y !== null ? ['x' => $image->focus_x, 'y' => $image->focus_y] : null,
|
||||
(bool) $image->no_crop,
|
||||
$caption,
|
||||
);
|
||||
|
||||
if ($path === null) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$image->forceFill(['variants' => array_merge((array) $image->variants, ['og' => $path])])->saveQuietly();
|
||||
$done++;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$this->info("OG-Varianten erzeugt: {$done}, übersprungen (keine Quelle): {$skipped}.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -137,3 +137,32 @@ test('teasers carry the credit in alt and title and the hero shows a caption', f
|
|||
->assertSee('alt="Teaser-Meldung · Foto: Anna Beispiel · KI-generiert"', false)
|
||||
->assertSee('data-image-credit-teaser', false);
|
||||
});
|
||||
|
||||
test('the regenerate command builds missing og variants for the existing stock with the credit line', function () {
|
||||
/** @var TestCase $this */
|
||||
$company = Company::factory()->presseecho()->create(['name' => 'Bestand GmbH', 'is_active' => true]);
|
||||
$pr = PressRelease::factory()->published()->for($company)->create(['published_ever_at' => now()->subDay()]);
|
||||
|
||||
$upload = UploadedFile::fake()->image('alt.jpg', 1600, 900)->size(100);
|
||||
$stored = (new ImageService)->storePressReleaseImage($upload, $pr->id, 'local');
|
||||
$variants = $stored['variants'];
|
||||
Storage::disk('local')->delete($variants['og']);
|
||||
unset($variants['og']);
|
||||
|
||||
$image = PressReleaseImage::factory()->for($pr)->legacy()->create(['disk' => 'local', 'path' => $stored['path'], 'variants' => $variants]);
|
||||
|
||||
$this->artisan('press-release-images:regenerate-variants', ['--only' => 'og', '--published' => true])
|
||||
->expectsOutputToContain('Trockenlauf: OG-Varianten für 1 von 1 Bildern.')
|
||||
->assertSuccessful();
|
||||
|
||||
expect($image->fresh()->variants)->not->toHaveKey('og');
|
||||
|
||||
$this->artisan('press-release-images:regenerate-variants', ['--only' => 'og', '--published' => true, '--execute' => true])
|
||||
->expectsOutputToContain('OG-Varianten erzeugt: 1')
|
||||
->assertSuccessful();
|
||||
|
||||
$og = $image->fresh()->variants['og'] ?? null;
|
||||
expect($og)->not->toBeNull()
|
||||
->and(Storage::disk('local')->exists($og))->toBeTrue()
|
||||
->and(getimagesize(Storage::disk('local')->path($og))[0])->toBe(1200);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ beforeEach(function (): void {
|
|||
/**
|
||||
* @return array{owner: User, pr: PressRelease}
|
||||
*/
|
||||
function gateDraft(): array
|
||||
function legalGateDraft(): array
|
||||
{
|
||||
$owner = User::factory()->create(['is_active' => true]);
|
||||
$owner->assignRole('customer');
|
||||
|
|
@ -49,7 +49,7 @@ function gateDraft(): array
|
|||
|
||||
test('with the gate off nothing is required and submitting works as before', function () {
|
||||
/** @var TestCase $this */
|
||||
['owner' => $owner, 'pr' => $pr] = gateDraft();
|
||||
['owner' => $owner, 'pr' => $pr] = legalGateDraft();
|
||||
$this->actingAs($owner);
|
||||
|
||||
expect(app(LegalAcceptanceGate::class)->requiredFor($owner))->toBeEmpty();
|
||||
|
|
@ -68,7 +68,7 @@ test('with the gate off nothing is required and submitting works as before', fun
|
|||
test('with the gate on the submit requires an active acceptance of agb and guidelines and records both', function () {
|
||||
/** @var TestCase $this */
|
||||
config()->set('legal.acceptance_gate', true);
|
||||
['owner' => $owner, 'pr' => $pr] = gateDraft();
|
||||
['owner' => $owner, 'pr' => $pr] = legalGateDraft();
|
||||
$this->actingAs($owner);
|
||||
|
||||
expect(app(LegalAcceptanceGate::class)->requiredFor($owner)->pluck('type')->all())->toBe(['agb', 'richtlinien']);
|
||||
|
|
@ -99,7 +99,7 @@ test('with the gate on the submit requires an active acceptance of agb and guide
|
|||
|
||||
test('acceptances are insert-only', function () {
|
||||
config()->set('legal.acceptance_gate', true);
|
||||
['owner' => $owner] = gateDraft();
|
||||
['owner' => $owner] = legalGateDraft();
|
||||
$gate = app(LegalAcceptanceGate::class);
|
||||
$rows = $gate->accept($owner, $gate->requiredFor($owner), 'gate', '203.0.113.5', 'Pest');
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ test('acceptances are insert-only', function () {
|
|||
test('the api answers 428 with the required documents until they are accepted', function () {
|
||||
/** @var TestCase $this */
|
||||
config()->set('legal.acceptance_gate', true);
|
||||
['owner' => $owner, 'pr' => $pr] = gateDraft();
|
||||
['owner' => $owner, 'pr' => $pr] = legalGateDraft();
|
||||
Sanctum::actingAs($owner, ['press-releases:write', 'press-releases:read']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pr->id}/submit", ['ai_text_declaration' => 'none'])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue