Add searchable participants via full text

This commit is contained in:
philipp lang 2024-12-11 22:32:23 +01:00
parent d03f036a2b
commit 54c37fccd1
7 changed files with 100 additions and 14 deletions

View File

@ -9,6 +9,7 @@ use App\Form\Scopes\ParticipantFilterScope;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Laravel\Scout\Builder;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantIndexAction class ParticipantIndexAction
@ -18,9 +19,10 @@ class ParticipantIndexAction
/** /**
* @return HasMany<Participant> * @return HasMany<Participant>
*/ */
protected function getQuery(Form $form, ParticipantFilterScope $filter): HasMany protected function getQuery(Form $form, ParticipantFilterScope $filter): Builder
{ {
return $form->participants()->withFilter($filter)->withCount('children')->with('form'); return $filter->setForm($form)->getQuery()
->query(fn ($q) => $q->withCount('children')->with('form'));
} }
/** /**

View File

@ -0,0 +1,27 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateParticipantSearchIndexAction
{
use AsAction;
public function handle(Form $form): void
{
$form->searchableUsing()->updateIndexSettings(
$form->participantsSearchableAs(),
[
'filterableAttributes' => $form->getFields()->getKeys(),
'searchableAttributes' => $form->getFields()->getKeys(),
'sortableAttributes' => [],
'displayedAttributes' => [...$form->getFields()->getKeys(), 'id'],
'pagination' => [
'maxTotalHits' => 1000000,
]
]
);
}
}

View File

@ -117,4 +117,12 @@ class FieldCollection extends Collection
{ {
return $this->first(fn ($field) => $field->specialType === $specialType); return $this->first(fn ($field) => $field->specialType === $specialType);
} }
/**
* @return array<int, string>
*/
public function getKeys(): array
{
return $this->map(fn ($field) => $field->key)->toArray();
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Form\Models; namespace App\Form\Models;
use App\Form\Actions\UpdateParticipantSearchIndexAction;
use App\Form\Data\ExportData; use App\Form\Data\ExportData;
use App\Form\Data\FieldCollection; use App\Form\Data\FieldCollection;
use App\Form\Data\FormConfigData; use App\Form\Data\FormConfigData;
@ -14,7 +15,6 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Laravel\Scout\Searchable; use Laravel\Scout\Searchable;
use Spatie\Image\Enums\Fit; use Spatie\Image\Enums\Fit;
use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\MediaLibrary\MediaCollections\Models\Media;
@ -172,5 +172,14 @@ class Form extends Model implements HasMedia
return; return;
} }
}); });
static::saved(function ($model) {
UpdateParticipantSearchIndexAction::dispatch($model);
});
}
public function participantsSearchableAs(): string
{
return config('scout.prefix') . 'forms_' . $this->id . '_participants';
} }
} }

View File

@ -15,6 +15,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Laravel\Scout\Searchable;
use stdClass; use stdClass;
class Participant extends Model implements Preventable class Participant extends Model implements Preventable
@ -22,6 +23,7 @@ class Participant extends Model implements Preventable
/** @use HasFactory<ParticipantFactory> */ /** @use HasFactory<ParticipantFactory> */
use HasFactory; use HasFactory;
use Searchable;
public $guarded = []; public $guarded = [];
@ -46,15 +48,6 @@ class Participant extends Model implements Preventable
return $this->hasMany(self::class, 'parent_id'); 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);
}
/** /**
* @return BelongsTo<Member, self> * @return BelongsTo<Member, self>
*/ */
@ -110,4 +103,14 @@ class Participant extends Model implements Preventable
{ {
return 'Nachweise erforderlich für deine Anmeldung zu ' . $this->form->name; return 'Nachweise erforderlich für deine Anmeldung zu ' . $this->form->name;
} }
public function searchableAs()
{
return $this->form->participantsSearchableAs();
}
public function toSearchableArray(): array
{
return $this->data;
}
} }

View File

@ -5,8 +5,9 @@ namespace App\Form\Scopes;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Lib\Filter; use App\Lib\Filter;
use Illuminate\Database\Eloquent\Builder; use App\Lib\ScoutFilter;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Laravel\Scout\Builder;
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;
@ -16,20 +17,32 @@ use Spatie\LaravelData\Mappers\SnakeCaseMapper;
*/ */
#[MapInputName(SnakeCaseMapper::class)] #[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)] #[MapOutputName(SnakeCaseMapper::class)]
class ParticipantFilterScope extends Filter class ParticipantFilterScope extends ScoutFilter
{ {
/** /**
* @param array<string, mixed> $data * @param array<string, mixed> $data
*/ */
public function __construct( public function __construct(
public array $data = [], public array $data = [],
public string $search = '',
public array $options = [],
) { ) {
} }
public static string $nan = 'deeb3ef4-d185-44b1-a4bc-0a4e7addebc3d8900c6f-a344-4afb-b54e-065ed483a7ba'; public static string $nan = 'deeb3ef4-d185-44b1-a4bc-0a4e7addebc3d8900c6f-a344-4afb-b54e-065ed483a7ba';
public Form $form;
public function getQuery(): Builder
{
$this->search = $this->search ?: '';
return Participant::search($this->search)->within($this->form->participantsSearchableAs());
}
public function setForm(Form $form): self public function setForm(Form $form): self
{ {
$this->form = $form;
foreach ($form->getFields() as $field) { foreach ($form->getFields() as $field) {
if (!Arr::has($this->data, $field->key)) { if (!Arr::has($this->data, $field->key)) {
data_set($this->data, $field->key, static::$nan); data_set($this->data, $field->key, static::$nan);

View File

@ -99,6 +99,30 @@ it('testItFiltersParticipantsByCheckboxValue', function () {
->assertJsonCount(2, 'data'); ->assertJsonCount(2, 'data');
}); });
it('test it handles full text search', function (array $memberAttributes, string $search, bool $includes) {
$this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()
->has(Participant::factory()->data(['vorname' => 'Max', 'select' => 'Pfadfinder', ...$memberAttributes]))
->fields([
$this->textField('vorname')->name('Vorname'),
$this->checkboxesField('select')->options(['Wölflinge', 'Pfadfinder']),
])
->create();
sleep(2);
$this->callFilter('form.participant.index', ['search' => $search], ['form' => $form])
->assertJsonCount($includes ? 1 : 0, 'data');
})->with([
[['vorname' => 'Max'], 'Max', true],
[['vorname' => 'Jane'], 'Max', false],
[['select' => 'Pfadfinder'], 'Pfadfinder', true],
[['select' => 'Pfadfinder'], 'Rov', false],
[['select' => 'Wölflinge'], 'Wölflinge', true],
[['select' => 'Wölflinge'], 'Wölf', true],
[['vorname' => 'Max', 'nachname' => 'Muster'], 'Max Muster', true],
[['vorname' => 'Max', 'nachname' => 'Muster'], 'Jane Doe', false],
]);
it('testItFiltersParticipantsByDropdownValue', function () { it('testItFiltersParticipantsByDropdownValue', function () {
$this->login()->loginNami()->withoutExceptionHandling(); $this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->fields([$this->dropdownField('drop')->options(['A', 'B'])]) $form = Form::factory()->fields([$this->dropdownField('drop')->options(['A', 'B'])])