WIP: Add cleanup for forms
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
@todo add cleanup icon as svg (broom.svg)
This commit is contained in:
parent
13b244ae33
commit
7f38d1b4ed
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormCleanupAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Form $form): void
|
||||
{
|
||||
$form->participants()->get()->each->delete();
|
||||
$form->save();
|
||||
ClearFrontendCacheAction::run();
|
||||
}
|
||||
|
||||
public function asController(Form $form): JsonResponse
|
||||
{
|
||||
$this->handle($form);
|
||||
|
||||
Succeeded::message('Teilnehmende gelöscht.')->dispatch();
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
||||
|
|
@ -190,7 +190,8 @@ class Form extends Model implements HasMedia
|
|||
return Sorting::from($this->meta['sorting']);
|
||||
}
|
||||
|
||||
public function isInDates(): bool {
|
||||
public function isInDates(): bool
|
||||
{
|
||||
if ($this->registration_from && $this->registration_from->gt(now())) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ class FormResource extends JsonResource
|
|||
'participant_root_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => -1]),
|
||||
'update' => route('form.update', $this->getModel()),
|
||||
'destroy' => route('form.destroy', $this->getModel()),
|
||||
'cleanup' => route('form.cleanup', $this->getModel()),
|
||||
'is_dirty' => route('form.is-dirty', $this->getModel()),
|
||||
'frontend' => str(app(FormSettings::class)->registerUrl)->replace('{slug}', $this->slug),
|
||||
'export' => route('form.export', $this->getModel()),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M236,40 L276,40 L276,270 L372,300 L372,340 L296,320 L296,460 L216,460 L216,320 L140,340 L140,300 L236,270 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 204 B |
|
|
@ -23,10 +23,23 @@
|
|||
<div v-show="active === 0" class="grid grid-cols-4 gap-3">
|
||||
<div class="flex space-x-3 col-span-2">
|
||||
<f-text id="name" v-model="single.name" class="grow" label="Name" required />
|
||||
<f-switch id="is_active" v-model="single.is_active" name="is_active" label="Aktiv" hint="Inaktive Veranstaltungen werden außerhalb von Adrema wie nicht existierende Veranstaltungen betrachtet. Insbesondere ist eine Anmeldung dann nicht möglich und die Veranstaltung erscheint auch nicht in der Veranstaltungs-Übersicht." />
|
||||
<f-switch id="is_private" v-model="single.is_private" name="is_private" label="Privat" hint="Ist eine Veranstaltung privat, so wird diese nicht auf der Website angezeigt. Eine Anmeldung ist jedoch trotzdem möglich, wenn man über den Anmelde-Link verfügt." />
|
||||
<f-switch
|
||||
id="is_active"
|
||||
v-model="single.is_active"
|
||||
name="is_active"
|
||||
label="Aktiv"
|
||||
hint="Inaktive Veranstaltungen werden außerhalb von Adrema wie nicht existierende Veranstaltungen betrachtet. Insbesondere ist eine Anmeldung dann nicht möglich und die Veranstaltung erscheint auch nicht in der Veranstaltungs-Übersicht."
|
||||
/>
|
||||
<f-switch
|
||||
id="is_private"
|
||||
v-model="single.is_private"
|
||||
name="is_private"
|
||||
label="Privat"
|
||||
hint="Ist eine Veranstaltung privat, so wird diese nicht auf der Website angezeigt. Eine Anmeldung ist jedoch trotzdem möglich, wenn man über den Anmelde-Link verfügt."
|
||||
/>
|
||||
</div>
|
||||
<f-singlefile id="header_image"
|
||||
<f-singlefile
|
||||
id="header_image"
|
||||
v-model="single.header_image"
|
||||
class="col-span-2"
|
||||
label="Bild"
|
||||
|
|
@ -41,9 +54,17 @@
|
|||
<f-text id="zip" v-model="single.zip" label="PLZ" />
|
||||
<f-text id="location" v-model="single.location" label="Ort" />
|
||||
<f-select id="country" v-model="single.country" class="col-span-2" name="country" label="Land" :options="meta.countries" />
|
||||
<f-text id="registration_from" v-model="single.registration_from" type="datetime-local" label="Registrierung von" hint="Ist eine Anmeldung laut dieser zwei Datumsangaben möglich, kann man sich anmelden. Andernfalls wird die Veranstaltung (mit Beschreibungstext) auf der Übersichtsseite angezeigt, man kommt allerdings nicht zum Anmeldeformular." required />
|
||||
<f-text
|
||||
id="registration_from"
|
||||
v-model="single.registration_from"
|
||||
type="datetime-local"
|
||||
label="Registrierung von"
|
||||
hint="Ist eine Anmeldung laut dieser zwei Datumsangaben möglich, kann man sich anmelden. Andernfalls wird die Veranstaltung (mit Beschreibungstext) auf der Übersichtsseite angezeigt, man kommt allerdings nicht zum Anmeldeformular."
|
||||
required
|
||||
/>
|
||||
<f-text id="registration_until" v-model="single.registration_until" type="datetime-local" label="Registrierung bis" required />
|
||||
<f-textarea id="excerpt"
|
||||
<f-textarea
|
||||
id="excerpt"
|
||||
v-model="single.excerpt"
|
||||
hint="Gebe hier eine kurze Beschreibung für die Veranstaltungs-Übersicht ein (Maximal 130 Zeichen)."
|
||||
label="Auszug"
|
||||
|
|
@ -61,9 +82,9 @@
|
|||
</div>
|
||||
<div v-show="active === 3" class="grid grid-cols-[1fr_300px] gap-3">
|
||||
<ui-note class="mt-2 col-span-full">
|
||||
Hier kannst du die E-Mail anpassen, die nach der Anmeldung an den Teilnehmer verschickt wird.<br>
|
||||
Es gibt dafür einen ersten E-Mail-Teil und einen zweiten E-Mail-Teil. Dazwischen werden die Daten des Teilnehmers aufgelistet.<br>
|
||||
Die Anrede ("Hallo Max Mustermann") wird automatisch an den Anfang gesetzt.<br>
|
||||
Hier kannst du die E-Mail anpassen, die nach der Anmeldung an den Teilnehmer verschickt wird.<br />
|
||||
Es gibt dafür einen ersten E-Mail-Teil und einen zweiten E-Mail-Teil. Dazwischen werden die Daten des Teilnehmers aufgelistet.<br />
|
||||
Die Anrede ("Hallo Max Mustermann") wird automatisch an den Anfang gesetzt.<br />
|
||||
Außerdem kannst du Dateien hochladen, die automatisch mit angehangen werden.
|
||||
</ui-note>
|
||||
<div>
|
||||
|
|
@ -79,7 +100,8 @@
|
|||
</template>
|
||||
</f-editor>
|
||||
</div>
|
||||
<f-multiplefiles id="mailattachments"
|
||||
<f-multiplefiles
|
||||
id="mailattachments"
|
||||
v-model="single.mailattachments"
|
||||
label="Anhänge"
|
||||
name="mailattachments"
|
||||
|
|
@ -105,7 +127,8 @@
|
|||
</div>
|
||||
<div v-show="active === 5" class="grid grid-cols-2 gap-3">
|
||||
<f-switch id="needs_prevention" v-model="single.needs_prevention" name="needs_prevention" label="Prävention" />
|
||||
<f-editor id="prevention_text"
|
||||
<f-editor
|
||||
id="prevention_text"
|
||||
v-model="single.prevention_text"
|
||||
hint="Wird an die Präventions-Email angehangen, die Teilnehmende dieser Veranstaltung erhalten"
|
||||
:rows="6"
|
||||
|
|
@ -182,7 +205,8 @@
|
|||
<ui-action-button tooltip="Nachmelde-Link kopieren" class="btn-info" icon="externallink" @click="copyLaterLink(form)" />
|
||||
<ui-action-button tooltip="Zuschuss-Liste erstellen" class="btn-info" icon="contribution" @click="onGenerateContribution(form)" />
|
||||
<ui-action-button :href="form.links.export" target="_BLANK" tooltip="als Tabellendokument exportieren" class="btn-info" icon="document" />
|
||||
<ui-action-button tooltip="Löschen" class="btn-danger" icon="trash" @click.prevent="onDelete(form)" />
|
||||
<ui-action-button tooltip="alle Teilnehmende löschen" class="btn-danger" icon="broom" @click.prevent="onCleanup(form)" />
|
||||
<ui-action-button tooltip="Veranstaltung löschen" class="btn-danger" icon="trash" @click.prevent="onDelete(form)" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -238,21 +262,28 @@ const allFields = computed(() => {
|
|||
});
|
||||
|
||||
async function onCopy(form) {
|
||||
await swal.confirm('Diese Veranstaltung kopieren?', 'Nach dem Kopieren wird die Veranstaltung auf inaktiv gesetzt. Bitte aktiviere den Filter "inaktive zeigen", um die kopierte Veranstaltung zu sehen.');
|
||||
await swal.confirm(
|
||||
'Diese Veranstaltung kopieren?',
|
||||
'Nach dem Kopieren wird die Veranstaltung auf inaktiv gesetzt. Bitte aktiviere den Filter "inaktive zeigen", um die kopierte Veranstaltung zu sehen.',
|
||||
);
|
||||
await axios.post(form.links.copy, {});
|
||||
reload(false);
|
||||
}
|
||||
|
||||
async function onGenerateContribution(form) {
|
||||
const response = await swal.ask('Zuschussliste erstellen', 'Hiermit erstellst du eine Zuschussliste mit allen angemeldeten Mitgliedern. Bite wähle aus, für welche Organisation du eine Liste erstellen willst.', [
|
||||
const response = await swal.ask(
|
||||
'Zuschussliste erstellen',
|
||||
'Hiermit erstellst du eine Zuschussliste mit allen angemeldeten Mitgliedern. Bite wähle aus, für welche Organisation du eine Liste erstellen willst.',
|
||||
[
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Organisation',
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: meta.value.contribution_types,
|
||||
}
|
||||
]);
|
||||
},
|
||||
],
|
||||
);
|
||||
await download(form.links.contribution, {type: response.type, validate: '1'});
|
||||
await download(form.links.contribution, {type: response.type});
|
||||
}
|
||||
|
|
@ -262,6 +293,12 @@ async function onDelete(form) {
|
|||
await remove(form);
|
||||
}
|
||||
|
||||
async function onCleanup(form) {
|
||||
await swal.confirm('Teilnehmende löschen?', `Alle Teilnehmenden der Veranstaltung ${form.name} werden unwiderruflich gelöscht. Die Veranstaltung selbst bleibt erhalten.`);
|
||||
await axios.delete(form.links.cleanup);
|
||||
reload(false);
|
||||
}
|
||||
|
||||
function setTemplate(template) {
|
||||
active.value = 0;
|
||||
single.value.config = template.config;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ use App\Fileshare\Actions\FileshareStoreAction;
|
|||
use App\Fileshare\Actions\FileshareUpdateAction;
|
||||
use App\Fileshare\Actions\ListFilesAction;
|
||||
use App\Form\Actions\ExportAction as ActionsExportAction;
|
||||
use App\Form\Actions\FormCleanupAction;
|
||||
use App\Form\Actions\FormCopyAction;
|
||||
use App\Form\Actions\FormDestroyAction;
|
||||
use App\Form\Actions\FormGenerateLaterlinkAction;
|
||||
|
|
@ -167,6 +168,7 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
|||
Route::get('/form', FormIndexAction::class)->name('form.index');
|
||||
Route::patch('/form/{form}', FormUpdateAction::class)->name('form.update');
|
||||
Route::delete('/form/{form}', FormDestroyAction::class)->name('form.destroy');
|
||||
Route::delete('/form/{form}/cleanup', FormCleanupAction::class)->name('form.cleanup');
|
||||
Route::post('/formtemplate', FormtemplateStoreAction::class)->name('formtemplate.store');
|
||||
Route::patch('/formtemplate/{formtemplate}', FormtemplateUpdateAction::class)->name('formtemplate.update');
|
||||
Route::delete('/formtemplate/{formtemplate}', FormtemplateDestroyAction::class)->name('formtemplate.destroy');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Form;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\Lib\CreatesFormFields;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(CreatesFormFields::class);
|
||||
|
||||
beforeEach(function () {
|
||||
test()->fakeMessages();
|
||||
test()->setUpForm();
|
||||
});
|
||||
|
||||
it('deletes all participants of a form', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory()->count(2))
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertDatabaseCount('participants', 0);
|
||||
});
|
||||
|
||||
it('does not delete the form itself', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertDatabaseHas('forms', ['id' => $form->id]);
|
||||
});
|
||||
|
||||
it('does not delete participants of other forms', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
$otherForm = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertDatabaseCount('participants', 1);
|
||||
test()->assertDatabaseHas('participants', ['id' => $otherForm->participants->first()->id]);
|
||||
});
|
||||
|
||||
it('deletes participants that are children of other participants', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()->sections([])->create();
|
||||
$parent = Participant::factory()->for($form)->create();
|
||||
Participant::factory()->for($form)->create(['parent_id' => $parent->id]);
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertDatabaseCount('participants', 0);
|
||||
});
|
||||
|
||||
it('clears the frontend cache', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertFrontendCacheCleared();
|
||||
});
|
||||
|
||||
it('shows success message', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertSuccessMessage('Teilnehmende gelöscht.');
|
||||
});
|
||||
Loading…
Reference in New Issue