feat: delete subscribers from page subscriber list
Adds DELETE route, form request authorization, admin UI with confirm, Dutch strings, and feature tests. Made-with: Cursor
This commit is contained in:
@@ -5,9 +5,11 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\DestroySubscriberRequest;
|
||||||
use App\Http\Requests\Admin\IndexSubscriberRequest;
|
use App\Http\Requests\Admin\IndexSubscriberRequest;
|
||||||
use App\Http\Requests\Admin\QueueMailwizzSyncRequest;
|
use App\Http\Requests\Admin\QueueMailwizzSyncRequest;
|
||||||
use App\Models\PreregistrationPage;
|
use App\Models\PreregistrationPage;
|
||||||
|
use App\Models\Subscriber;
|
||||||
use App\Services\DispatchUnsyncedMailwizzSyncJobsService;
|
use App\Services\DispatchUnsyncedMailwizzSyncJobsService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
@@ -32,6 +34,15 @@ class SubscriberController extends Controller
|
|||||||
return view('admin.subscribers.index', compact('page', 'subscribers', 'unsyncedMailwizzCount'));
|
return view('admin.subscribers.index', compact('page', 'subscribers', 'unsyncedMailwizzCount'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function destroy(DestroySubscriberRequest $request, PreregistrationPage $page, Subscriber $subscriber): RedirectResponse
|
||||||
|
{
|
||||||
|
$subscriber->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.pages.subscribers.index', $page)
|
||||||
|
->with('status', __('Subscriber removed.'));
|
||||||
|
}
|
||||||
|
|
||||||
public function queueMailwizzSync(
|
public function queueMailwizzSync(
|
||||||
QueueMailwizzSyncRequest $request,
|
QueueMailwizzSyncRequest $request,
|
||||||
PreregistrationPage $page,
|
PreregistrationPage $page,
|
||||||
|
|||||||
36
app/Http/Requests/Admin/DestroySubscriberRequest.php
Normal file
36
app/Http/Requests/Admin/DestroySubscriberRequest.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use App\Models\PreregistrationPage;
|
||||||
|
use App\Models\Subscriber;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class DestroySubscriberRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
$page = $this->route('page');
|
||||||
|
$subscriber = $this->route('subscriber');
|
||||||
|
if (! $page instanceof PreregistrationPage || ! $subscriber instanceof Subscriber) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($subscriber->preregistration_page_id !== $page->id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->user()?->can('update', $page) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<int, ValidationRule|string>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,5 +18,9 @@
|
|||||||
"Thank you for registering!": "Bedankt voor je registratie!",
|
"Thank you for registering!": "Bedankt voor je registratie!",
|
||||||
"You are already registered for this event.": "Je bent al geregistreerd voor dit evenement.",
|
"You are already registered for this event.": "Je bent al geregistreerd voor dit evenement.",
|
||||||
"Please enter a valid email address.": "Voer een geldig e-mailadres in.",
|
"Please enter a valid email address.": "Voer een geldig e-mailadres in.",
|
||||||
"Please enter a valid phone number (8–15 digits).": "Voer een geldig telefoonnummer in (8 tot 15 cijfers)."
|
"Please enter a valid phone number (8–15 digits).": "Voer een geldig telefoonnummer in (8 tot 15 cijfers).",
|
||||||
|
"Subscriber removed.": "Abonnee verwijderd.",
|
||||||
|
"Delete this subscriber? This cannot be undone.": "Deze abonnee verwijderen? Dit kan niet ongedaan worden gemaakt.",
|
||||||
|
"Remove": "Verwijderen",
|
||||||
|
"Actions": "Acties"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
@endif
|
@endif
|
||||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Registered at') }}</th>
|
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Registered at') }}</th>
|
||||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Mailwizz') }}</th>
|
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Mailwizz') }}</th>
|
||||||
|
<th class="w-px whitespace-nowrap px-4 py-3 font-semibold text-slate-700">{{ __('Actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-slate-100">
|
<tbody class="divide-y divide-slate-100">
|
||||||
@@ -77,10 +78,29 @@
|
|||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-right">
|
||||||
|
@can('update', $page)
|
||||||
|
<form
|
||||||
|
method="post"
|
||||||
|
action="{{ route('admin.pages.subscribers.destroy', [$page, $subscriber]) }}"
|
||||||
|
class="inline"
|
||||||
|
onsubmit="return confirm(@js(__('Delete this subscriber? This cannot be undone.')));"
|
||||||
|
>
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-lg border border-red-200 bg-white px-2.5 py-1 text-xs font-semibold text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
{{ __('Remove') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endcan
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{ $page->isPhoneFieldEnabledForSubscribers() ? 6 : 5 }}" class="px-4 py-12 text-center text-slate-500">
|
<td colspan="{{ $page->isPhoneFieldEnabledForSubscribers() ? 7 : 6 }}" class="px-4 py-12 text-center text-slate-500">
|
||||||
{{ __('No subscribers match your criteria.') }}
|
{{ __('No subscribers match your criteria.') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(
|
|||||||
|
|
||||||
// Subscribers (nested under pages) — export before index so the path is unambiguous
|
// Subscribers (nested under pages) — export before index so the path is unambiguous
|
||||||
Route::get('pages/{page}/subscribers/export', [SubscriberController::class, 'export'])->name('pages.subscribers.export');
|
Route::get('pages/{page}/subscribers/export', [SubscriberController::class, 'export'])->name('pages.subscribers.export');
|
||||||
|
Route::delete('pages/{page}/subscribers/{subscriber}', [SubscriberController::class, 'destroy'])->name('pages.subscribers.destroy');
|
||||||
Route::post('pages/{page}/subscribers/queue-mailwizz-sync', [SubscriberController::class, 'queueMailwizzSync'])->name('pages.subscribers.queue-mailwizz-sync');
|
Route::post('pages/{page}/subscribers/queue-mailwizz-sync', [SubscriberController::class, 'queueMailwizzSync'])->name('pages.subscribers.queue-mailwizz-sync');
|
||||||
Route::get('pages/{page}/subscribers', [SubscriberController::class, 'index'])->name('pages.subscribers.index');
|
Route::get('pages/{page}/subscribers', [SubscriberController::class, 'index'])->name('pages.subscribers.index');
|
||||||
|
|
||||||
|
|||||||
131
tests/Feature/DestroySubscriberTest.php
Normal file
131
tests/Feature/DestroySubscriberTest.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\PreregistrationPage;
|
||||||
|
use App\Models\Subscriber;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DestroySubscriberTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_page_owner_can_delete_subscriber_on_that_page(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create(['role' => 'user']);
|
||||||
|
$page = PreregistrationPage::query()->create([
|
||||||
|
'slug' => (string) Str::uuid(),
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'title' => 'Fest',
|
||||||
|
'heading' => 'Fest',
|
||||||
|
'intro_text' => null,
|
||||||
|
'thank_you_message' => null,
|
||||||
|
'expired_message' => null,
|
||||||
|
'ticketshop_url' => null,
|
||||||
|
'start_date' => now()->subDay(),
|
||||||
|
'end_date' => now()->addMonth(),
|
||||||
|
'phone_enabled' => false,
|
||||||
|
'background_image' => null,
|
||||||
|
'logo_image' => null,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
$subscriber = Subscriber::query()->create([
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'first_name' => 'Ada',
|
||||||
|
'last_name' => 'Lovelace',
|
||||||
|
'email' => 'ada@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->delete(route('admin.pages.subscribers.destroy', [$page, $subscriber]));
|
||||||
|
|
||||||
|
$response->assertRedirect(route('admin.pages.subscribers.index', $page));
|
||||||
|
$response->assertSessionHas('status');
|
||||||
|
$this->assertDatabaseMissing('subscribers', ['id' => $subscriber->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_other_user_cannot_delete_subscriber(): void
|
||||||
|
{
|
||||||
|
$owner = User::factory()->create(['role' => 'user']);
|
||||||
|
$intruder = User::factory()->create(['role' => 'user']);
|
||||||
|
$page = PreregistrationPage::query()->create([
|
||||||
|
'slug' => (string) Str::uuid(),
|
||||||
|
'user_id' => $owner->id,
|
||||||
|
'title' => 'Fest',
|
||||||
|
'heading' => 'Fest',
|
||||||
|
'intro_text' => null,
|
||||||
|
'thank_you_message' => null,
|
||||||
|
'expired_message' => null,
|
||||||
|
'ticketshop_url' => null,
|
||||||
|
'start_date' => now()->subDay(),
|
||||||
|
'end_date' => now()->addMonth(),
|
||||||
|
'phone_enabled' => false,
|
||||||
|
'background_image' => null,
|
||||||
|
'logo_image' => null,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
$subscriber = Subscriber::query()->create([
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'first_name' => 'A',
|
||||||
|
'last_name' => 'B',
|
||||||
|
'email' => 'x@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($intruder)->delete(route('admin.pages.subscribers.destroy', [$page, $subscriber]));
|
||||||
|
|
||||||
|
$response->assertForbidden();
|
||||||
|
$this->assertDatabaseHas('subscribers', ['id' => $subscriber->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_cannot_delete_subscriber_using_wrong_page_in_url(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create(['role' => 'user']);
|
||||||
|
$pageA = PreregistrationPage::query()->create([
|
||||||
|
'slug' => (string) Str::uuid(),
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'title' => 'A',
|
||||||
|
'heading' => 'A',
|
||||||
|
'intro_text' => null,
|
||||||
|
'thank_you_message' => null,
|
||||||
|
'expired_message' => null,
|
||||||
|
'ticketshop_url' => null,
|
||||||
|
'start_date' => now()->subDay(),
|
||||||
|
'end_date' => now()->addMonth(),
|
||||||
|
'phone_enabled' => false,
|
||||||
|
'background_image' => null,
|
||||||
|
'logo_image' => null,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
$pageB = PreregistrationPage::query()->create([
|
||||||
|
'slug' => (string) Str::uuid(),
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'title' => 'B',
|
||||||
|
'heading' => 'B',
|
||||||
|
'intro_text' => null,
|
||||||
|
'thank_you_message' => null,
|
||||||
|
'expired_message' => null,
|
||||||
|
'ticketshop_url' => null,
|
||||||
|
'start_date' => now()->subDay(),
|
||||||
|
'end_date' => now()->addMonth(),
|
||||||
|
'phone_enabled' => false,
|
||||||
|
'background_image' => null,
|
||||||
|
'logo_image' => null,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
$subscriber = Subscriber::query()->create([
|
||||||
|
'preregistration_page_id' => $pageB->id,
|
||||||
|
'first_name' => 'A',
|
||||||
|
'last_name' => 'B',
|
||||||
|
'email' => 'y@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->delete(route('admin.pages.subscribers.destroy', [$pageA, $subscriber]));
|
||||||
|
|
||||||
|
$response->assertForbidden();
|
||||||
|
$this->assertDatabaseHas('subscribers', ['id' => $subscriber->id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user