-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add photo export functionality * Add Export Data button * Refactor to an Action * Complete test for exporting photo data * Add tooltip for exporting data * Fix exporting large amounts of data
- Loading branch information
Showing
10 changed files
with
258 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?php | ||
|
||
namespace App\Actions\Photos; | ||
|
||
use App\DTO\PhotoExport; | ||
use App\Models\User; | ||
use Generator; | ||
use Illuminate\Contracts\Database\Eloquent\Builder; | ||
|
||
class ExportPhotosAction | ||
{ | ||
public function run(User $user): Generator | ||
{ | ||
$photos = $user | ||
->photos() | ||
->filter($user->settings->photo_filters) | ||
->with(['photoItems' => fn (Builder $q) => $q | ||
->with('item:id,name') | ||
->with('tags:id,name'), | ||
]) | ||
->lazyById(); | ||
|
||
foreach ($photos as $photo) { | ||
yield PhotoExport::fromModel($photo); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<?php | ||
|
||
namespace App\DTO; | ||
|
||
use App\Models\Photo; | ||
use App\Models\PhotoItem; | ||
use Illuminate\Support\Collection; | ||
use Spatie\LaravelData\Data; | ||
|
||
/** @phpstan-consistent-constructor */ | ||
class PhotoExport extends Data | ||
{ | ||
/** | ||
* @param Collection<int, array<int, mixed>> $items | ||
*/ | ||
public function __construct( | ||
public int $id, | ||
public string $original_file_name, | ||
public ?float $latitude, | ||
public ?float $longitude, | ||
public ?string $taken_at_local, | ||
public string $created_at, | ||
public Collection $items, | ||
) { | ||
} | ||
|
||
public static function fromModel(Photo $photo): static | ||
{ | ||
return new static( | ||
id: $photo->id, | ||
original_file_name: $photo->original_file_name, | ||
latitude: $photo->latitude, | ||
longitude: $photo->longitude, | ||
taken_at_local: $photo->taken_at_local, | ||
created_at: $photo->created_at?->toIso8601String(), | ||
items: $photo->photoItems->map(fn (PhotoItem $photoItem): array => [ | ||
'name' => $photoItem->item?->name, | ||
'picked_up' => $photoItem->picked_up, | ||
'recycled' => $photoItem->recycled, | ||
'deposit' => $photoItem->deposit, | ||
'quantity' => $photoItem->quantity, | ||
'tags' => $photoItem->tags->pluck('name')->toArray(), | ||
]), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
namespace App\Http\Controllers\Photos; | ||
|
||
use App\Actions\Photos\ExportPhotosAction; | ||
use App\Http\Controllers\Controller; | ||
use App\Models\User; | ||
use Symfony\Component\HttpFoundation\StreamedResponse; | ||
|
||
class ExportPhotosController extends Controller | ||
{ | ||
public function __invoke(ExportPhotosAction $action): StreamedResponse | ||
{ | ||
/** @var User $user */ | ||
$user = auth()->user(); | ||
|
||
$photos = $action->run($user); | ||
|
||
return response()->streamJson(['photos' => $photos], 200, [ | ||
'Content-Type' => 'application/json', | ||
'Content-Disposition' => 'attachment; filename="photos.json"', | ||
]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?php | ||
|
||
use App\Models\Photo; | ||
use App\Models\User; | ||
|
||
test('a user can export their photos', function (): void { | ||
$this->actingAs($user = User::factory()->create()); | ||
|
||
$photo = Photo::factory()->for($user)->create(); | ||
|
||
$response = $this->get('/photos/export'); | ||
|
||
$response->assertOk(); | ||
$response->assertDownload('photos.json'); | ||
$response->assertStreamedJsonContent([ | ||
'photos' => [[ | ||
'id' => $photo->id, | ||
'original_file_name' => $photo->original_file_name, | ||
'latitude' => $photo->latitude, | ||
'longitude' => $photo->longitude, | ||
'taken_at_local' => $photo->taken_at_local, | ||
'created_at' => $photo->created_at->toIso8601String(), | ||
'items' => [], | ||
]], | ||
]); | ||
}); | ||
|
||
test('if there are no photos the export still works', function (): void { | ||
$this->actingAs($user = User::factory()->create()); | ||
|
||
$response = $this->get('/photos/export'); | ||
|
||
$response->assertOk(); | ||
$response->assertDownload('photos.json'); | ||
$response->assertStreamedJsonContent([ | ||
'photos' => [], | ||
]); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
<?php | ||
|
||
use App\Actions\Photos\ExportPhotosAction; | ||
use App\DTO\PhotoExport; | ||
use App\DTO\PhotoFilters; | ||
use App\DTO\UserSettings; | ||
use App\Models\Photo; | ||
use App\Models\PhotoItem; | ||
use App\Models\Tag; | ||
use App\Models\User; | ||
use Illuminate\Support\Collection; | ||
|
||
use function Pest\Laravel\freezeTime; | ||
|
||
it('exports photos with items and tags', function (): void { | ||
freezeTime(); | ||
$user = User::factory()->create(); | ||
$photo = Photo::factory()->create(['user_id' => $user->id]); | ||
$photoItem = PhotoItem::factory()->create([ | ||
'photo_id' => $photo->id, | ||
'picked_up' => true, | ||
'recycled' => false, | ||
'deposit' => true, | ||
'quantity' => 2, | ||
]); | ||
$tag = Tag::factory()->create(); | ||
$photoItem->tags()->attach($tag->id); | ||
|
||
$exportPhotosAction = new ExportPhotosAction(); | ||
$result = $exportPhotosAction->run($user); | ||
|
||
expect($result)->toBeInstanceOf(Generator::class); | ||
|
||
$result = iterator_to_array($result); | ||
|
||
expect($result)->toHaveCount(1) | ||
->and($result[0])->toBeInstanceOf(PhotoExport::class) | ||
->id->toBe($photo->id) | ||
->original_file_name->toBe($photo->original_file_name) | ||
->latitude->toBe($photo->latitude) | ||
->longitude->toBe($photo->longitude) | ||
->taken_at_local->toBe($photo->taken_at_local) | ||
->created_at->toEqual($photo->created_at->toIso8601String()) | ||
->items->toBeInstanceOf(Collection::class) | ||
->items->toHaveCount(1) | ||
->items->first()->toBe([ | ||
'name' => $photoItem->item->name, | ||
'picked_up' => $photoItem->picked_up, | ||
'recycled' => $photoItem->recycled, | ||
'deposit' => $photoItem->deposit, | ||
'quantity' => $photoItem->quantity, | ||
'tags' => [$tag->name], | ||
]); | ||
}); | ||
|
||
it('follows user defined filters when returning a response', function (): void { | ||
freezeTime(); | ||
$user = User::factory()->create([ | ||
'settings' => new UserSettings(photo_filters: new PhotoFilters(picked_up: true)), | ||
]); | ||
|
||
$pickedUpPhoto = Photo::factory()->create(['user_id' => $user->id]); | ||
$pickedUpPhotoItem = PhotoItem::factory()->create(['photo_id' => $pickedUpPhoto->id, 'picked_up' => true]); | ||
|
||
$notPickedUpPhoto = Photo::factory()->create(['user_id' => $user->id]); | ||
$notPickedUpPhotoItem = PhotoItem::factory()->create(['photo_id' => $notPickedUpPhoto->id, 'picked_up' => false]); | ||
|
||
$exportPhotosAction = new ExportPhotosAction(); | ||
$result = $exportPhotosAction->run($user); | ||
|
||
expect($result)->toBeInstanceOf(Generator::class); | ||
|
||
$result = iterator_to_array($result); | ||
|
||
expect($result)->toHaveCount(1) | ||
->and($result[0]->id)->toBe($pickedUpPhoto->id); | ||
}); |