Add participants parent and child in request filter

This commit is contained in:
philipp lang 2024-03-08 02:19:07 +01:00
parent a0842afd47
commit 78d6c6d864
12 changed files with 147 additions and 21 deletions

View File

@ -2,7 +2,7 @@
namespace App\Form\Actions; namespace App\Form\Actions;
use App\Form\FilterScope; use App\Form\Scopes\FormFilterScope;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Resources\FormApiResource; use App\Form\Resources\FormApiResource;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
@ -20,7 +20,7 @@ class FormApiListAction
*/ */
public function handle(string $filter, int $perPage): LengthAwarePaginator public function handle(string $filter, int $perPage): LengthAwarePaginator
{ {
return FilterScope::fromRequest($filter)->getQuery()->paginate($perPage); return FormFilterScope::fromRequest($filter)->getQuery()->paginate($perPage);
} }
public function asController(ActionRequest $request): AnonymousResourceCollection public function asController(ActionRequest $request): AnonymousResourceCollection

View File

@ -2,7 +2,7 @@
namespace App\Form\Actions; namespace App\Form\Actions;
use App\Form\FilterScope; use App\Form\Scopes\FormFilterScope;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Resources\FormResource; use App\Form\Resources\FormResource;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
@ -20,7 +20,7 @@ class FormIndexAction
*/ */
public function handle(string $filter): LengthAwarePaginator public function handle(string $filter): LengthAwarePaginator
{ {
return FilterScope::fromRequest($filter)->getQuery()->query(fn ($query) => $query->withCount('participants'))->paginate(15); return FormFilterScope::fromRequest($filter)->getQuery()->query(fn ($query) => $query->withCount('participants'))->paginate(15);
} }
public function asController(ActionRequest $request): Response public function asController(ActionRequest $request): Response

View File

@ -5,6 +5,7 @@ namespace App\Form\Actions;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Form\Resources\ParticipantResource; use App\Form\Resources\ParticipantResource;
use App\Form\Scopes\ParticipantFilterScope;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@ -16,14 +17,15 @@ class ParticipantIndexAction
/** /**
* @return LengthAwarePaginator<Participant> * @return LengthAwarePaginator<Participant>
*/ */
public function handle(Form $form): LengthAwarePaginator public function handle(Form $form, ParticipantFilterScope $filter): LengthAwarePaginator
{ {
return $form->participants()->with('form')->paginate(15); return $form->participants()->withFilter($filter)->with('form')->paginate(15);
} }
public function asController(Form $form): AnonymousResourceCollection public function asController(Form $form): AnonymousResourceCollection
{ {
return ParticipantResource::collection($this->handle($form)) $filter = ParticipantFilterScope::fromRequest(request()->input('filter'));
return ParticipantResource::collection($this->handle($form, $filter))
->additional(['meta' => ParticipantResource::meta($form)]); ->additional(['meta' => ParticipantResource::meta($form)]);
} }
} }

View File

@ -2,9 +2,12 @@
namespace App\Form\Models; namespace App\Form\Models;
use App\Form\Scopes\ParticipantFilterScope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Participant extends Model class Participant extends Model
{ {
@ -23,4 +26,21 @@ class Participant extends Model
{ {
return $this->belongsTo(Form::class); return $this->belongsTo(Form::class);
} }
/**
* @return HasMany<self>
*/
public function children(): HasMany
{
return $this->hasMany(self::class, 'parent_id');
}
/**
* @param Builder<self> $query
* @return Builder<self>
*/
public function scopeWithFilter(Builder $query, ParticipantFilterScope $filter): Builder
{
return $filter->apply($query);
}
} }

View File

@ -4,7 +4,7 @@ namespace App\Form\Resources;
use App\Form\Enums\NamiType; use App\Form\Enums\NamiType;
use App\Form\Fields\Field; use App\Form\Fields\Field;
use App\Form\FilterScope; use App\Form\Scopes\FormFilterScope;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Formtemplate; use App\Form\Models\Formtemplate;
use App\Group; use App\Group;
@ -59,7 +59,7 @@ class FormResource extends JsonResource
'base_url' => url(''), 'base_url' => url(''),
'groups' => Group::forSelect(), 'groups' => Group::forSelect(),
'fields' => Field::asMeta(), 'fields' => Field::asMeta(),
'filter' => FilterScope::fromRequest(request()->input('filter', '')), 'filter' => FormFilterScope::fromRequest(request()->input('filter', '')),
'links' => [ 'links' => [
'store' => route('form.store'), 'store' => route('form.store'),
'formtemplate_index' => route('formtemplate.index'), 'formtemplate_index' => route('formtemplate.index'),

View File

@ -1,20 +1,20 @@
<?php <?php
namespace App\Form; namespace App\Form\Scopes;
use Laravel\Scout\Builder; use Laravel\Scout\Builder;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Lib\Filter; use App\Lib\ScoutFilter;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Mappers\SnakeCaseMapper;
/** /**
* @extends Filter<Form> * @extends ScoutFilter<Form>
*/ */
#[MapInputName(SnakeCaseMapper::class)] #[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)] #[MapOutputName(SnakeCaseMapper::class)]
class FilterScope extends Filter class FormFilterScope extends ScoutFilter
{ {
public function __construct( public function __construct(
public ?string $search = '', public ?string $search = '',

View File

@ -0,0 +1,39 @@
<?php
namespace App\Form\Scopes;
use App\Form\Models\Participant;
use App\Lib\Filter;
use Illuminate\Database\Eloquent\Builder;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
/**
* @extends Filter<Participant>
*/
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class ParticipantFilterScope extends Filter
{
public function __construct(
public ?int $parent = null,
) {
}
/**
* @inheritdoc
*/
public function apply(Builder $query): Builder
{
if ($this->parent === -1) {
$query = $query->whereNull('parent_id');
}
if (!is_null($this->parent) && $this->parent > 0) {
$query = $query->where('parent_id', $this->parent);
}
return $query;
}
}

View File

@ -2,8 +2,8 @@
namespace App\Lib; namespace App\Lib;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Builder;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
/** /**
@ -14,10 +14,10 @@ abstract class Filter extends Data
{ {
/** /**
* @return Builder * @param Builder<T> $query
* @return Builder<T>
*/ */
abstract public function getQuery(): Builder; abstract public function apply(Builder $query): Builder;
protected Builder $query;
/** /**
* @param array<string, mixed>|string|null $request * @param array<string, mixed>|string|null $request

41
app/Lib/ScoutFilter.php Normal file
View File

@ -0,0 +1,41 @@
<?php
namespace App\Lib;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Builder;
use Spatie\LaravelData\Data;
/**
* @template T of Model
* @property Builder $query
*/
abstract class ScoutFilter extends Data
{
/**
* @return Builder
*/
abstract public function getQuery(): Builder;
protected Builder $query;
/**
* @param array<string, mixed>|string|null $request
*/
public static function fromRequest(array|string|null $request = null): static
{
$payload = is_string($request)
? json_decode(rawurldecode(base64_decode($request)), true)
: $request;
return static::fromPost($payload);
}
/**
* @param array<string, mixed> $post
*/
public static function fromPost(?array $post = null): static
{
return static::withoutMagicalCreationFrom($post ?: []);
}
}

View File

@ -5,8 +5,8 @@ namespace App\Member;
use App\Activity; use App\Activity;
use App\Group; use App\Group;
use App\Invoice\BillKind; use App\Invoice\BillKind;
use App\Lib\Filter;
use App\Subactivity; use App\Subactivity;
use App\Lib\ScoutFilter;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Laravel\Scout\Builder; use Laravel\Scout\Builder;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
@ -14,11 +14,11 @@ use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Mappers\SnakeCaseMapper;
/** /**
* @extends Filter<Member> * @extends ScoutFilter<Member>
*/ */
#[MapInputName(SnakeCaseMapper::class)] #[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)] #[MapOutputName(SnakeCaseMapper::class)]
class FilterScope extends Filter class FilterScope extends ScoutFilter
{ {
/** /**
* @param array<int, int> $activityIds * @param array<int, int> $activityIds

View File

@ -17,7 +17,7 @@ return new class extends Migration
$table->id(); $table->id();
$table->json('data'); $table->json('data');
$table->foreignId('form_id'); $table->foreignId('form_id');
$table->foreignId('parent_id', 'participants')->nullable(); $table->foreignId('parent_id')->nullable();
$table->string('mitgliedsnr')->nullable(); $table->string('mitgliedsnr')->nullable();
$table->timestamps(); $table->timestamps();
}); });

View File

@ -54,6 +54,30 @@ class ParticipantIndexActionTest extends FormTestCase
->assertJsonPath('meta.form_meta.sorting', ['vorname', 'asc']); ->assertJsonPath('meta.form_meta.sorting', ['vorname', 'asc']);
} }
public function testItShowsOnlyRootMembers(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->create();
Participant::factory()->for($form)->count(2)
->has(Participant::factory()->count(3)->for($form), 'children')
->create();
$this->callFilter('form.participant.index', ['parent' => -1], ['form' => $form])->assertJsonCount(2, 'data');
$this->callFilter('form.participant.index', ['parent' => null], ['form' => $form])->assertJsonCount(8, 'data');
}
public function testItShowsChildrenOfParent(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->create();
$parents = Participant::factory()->for($form)->count(2)
->has(Participant::factory()->count(3)->for($form), 'children')
->create();
$this->callFilter('form.participant.index', ['parent' => $parents->get(0)->id], ['form' => $form])->assertJsonCount(3, 'data');
$this->callFilter('form.participant.index', ['parent' => $parents->get(1)->id], ['form' => $form])->assertJsonCount(3, 'data');
}
public function testItPresentsNamiField(): void public function testItPresentsNamiField(): void
{ {
$this->login()->loginNami()->withoutExceptionHandling(); $this->login()->loginNami()->withoutExceptionHandling();