Unified user notifications

This commit is contained in:
Igor Scheller 2023-02-02 22:53:51 +01:00
parent 1fe30fc82f
commit 713f8222e4
43 changed files with 313 additions and 213 deletions

View File

@ -88,7 +88,7 @@ function angeltype_delete_controller()
if (request()->hasPostData('delete')) {
$angeltype->delete();
engelsystem_log('Deleted angeltype: ' . AngelType_name_render($angeltype, true));
success(sprintf(__('Angeltype %s deleted.'), AngelType_name_render($angeltype)));
success(sprintf(__('Angeltype %s deleted.'), $angeltype->name));
throw_redirect(page_link_to('angeltypes'));
}

View File

@ -108,7 +108,7 @@ function shift_entry_create_controller_admin(Shift $shift, ?AngelType $angeltype
$shiftEntry->save();
ShiftEntry_onCreate($shiftEntry);
success(sprintf(__('%s has been subscribed to the shift.'), User_Nick_render($signup_user)));
success(sprintf(__('%s has been subscribed to the shift.'), $signup_user->name));
throw_redirect(shift_link($shift));
}
@ -157,7 +157,7 @@ function shift_entry_create_controller_supporter(Shift $shift, AngelType $angelt
$shiftEntry->save();
ShiftEntry_onCreate($shiftEntry);
success(sprintf(__('%s has been subscribed to the shift.'), User_Nick_render($signup_user)));
success(sprintf(__('%s has been subscribed to the shift.'), $signup_user->name));
throw_redirect(shift_link($shift));
}

View File

@ -82,7 +82,7 @@ function user_angeltypes_delete_all_controller(): array
->delete();
engelsystem_log(sprintf('Denied all users for angeltype %s', AngelType_name_render($angeltype, true)));
success(sprintf(__('Denied all users for angeltype %s.'), AngelType_name_render($angeltype)));
success(sprintf(__('Denied all users for angeltype %s.'), $angeltype->name));
throw_redirect(page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]));
}
@ -121,7 +121,7 @@ function user_angeltypes_confirm_all_controller(): array
->update(['confirm_user_id' => $user->id]);
engelsystem_log(sprintf('Confirmed all users for angeltype %s', AngelType_name_render($angeltype, true)));
success(sprintf(__('Confirmed all users for angeltype %s.'), AngelType_name_render($angeltype)));
success(sprintf(__('Confirmed all users for angeltype %s.'), $angeltype->name));
foreach ($users as $user) {
user_angeltype_confirm_email($user, $angeltype);
@ -169,11 +169,7 @@ function user_angeltype_confirm_controller(): array
User_Nick_render($user_source, true),
AngelType_name_render($angeltype, true)
));
success(sprintf(
__('%s confirmed for angeltype %s.'),
User_Nick_render($user_source),
AngelType_name_render($angeltype)
));
success(sprintf(__('%s confirmed for angeltype %s.'), $user_source->name, $angeltype->name));
user_angeltype_confirm_email($user_source, $angeltype);
@ -268,7 +264,7 @@ function user_angeltype_delete_controller(): array
$user_angeltype->delete();
engelsystem_log(sprintf('User %s removed from %s.', User_Nick_render($user_source, true), $angeltype->name));
success(sprintf(__('User %s removed from %s.'), User_Nick_render($user_source), $angeltype->name));
success(sprintf(__('User %s removed from %s.'), $user_source->name, $angeltype->name));
throw_redirect(page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]));
}
@ -323,11 +319,7 @@ function user_angeltype_update_controller(): array
AngelType_name_render($angeltype, true),
User_Nick_render($user_source, true)
));
success(sprintf(
$msg,
AngelType_name_render($angeltype),
User_Nick_render($user_source)
));
success(sprintf($msg, $angeltype->name, $user_source->name));
throw_redirect(page_link_to('angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]));
}
@ -375,11 +367,7 @@ function user_angeltype_add_controller(): array
User_Nick_render($user_source, true),
AngelType_name_render($angeltype, true)
));
success(sprintf(
__('User %s added to %s.'),
User_Nick_render($user_source),
AngelType_name_render($angeltype)
));
success(sprintf(__('User %s added to %s.'), $user_source->name, $angeltype->name));
if ($request->hasPostData('auto_confirm_user')) {
$userAngelType->confirmUser()->associate($user_source);

View File

@ -48,7 +48,7 @@ function engelsystem_email_to_user($recipientUser, $title, $message, $notIfItsMe
$translator->setLocale($locale);
if (!$status) {
error(sprintf(__('User %s could not be notified by email due to an error.'), User_Nick_render($recipientUser)));
error(sprintf(__('User %s could not be notified by email due to an error.'), $recipientUser->name));
engelsystem_log(sprintf('User %s could not be notified by email due to an error.', $recipientUser->name));
}

View File

@ -1,40 +1,15 @@
<?php
use Engelsystem\Controllers\NotificationType;
/**
* Returns messages from session and removes them from the stack
* @param bool $includeMessagesFromNewProcedure
* If set, the messages from the new procedure are also included.
* The output will be similar to how it would be with messages.twig.
* @see \Engelsystem\Controllers\HasUserNotifications
* Returns messages from session and removes them from the stack by rendering the messages twig template
* @return string
* @see \Engelsystem\Controllers\HasUserNotifications
*/
function msg(bool $includeMessagesFromNewProcedure = false)
function msg()
{
$session = session();
$message = $session->get('msg', '');
$session->set('msg', '');
if ($includeMessagesFromNewProcedure) {
foreach (session()->get('errors', []) as $msg) {
$message .= error(__($msg), true);
}
foreach (session()->get('warnings', []) as $msg) {
$message .= warning(__($msg), true);
}
foreach (session()->get('information', []) as $msg) {
$message .= info(__($msg), true);
}
foreach (session()->get('messages', []) as $msg) {
$message .= success(__($msg), true);
}
foreach (['errors', 'warnings', 'information', 'messages'] as $type) {
session()->remove($type);
}
}
return $message;
return view('layouts/parts/messages.twig');
}
/**
@ -46,7 +21,7 @@ function msg(bool $includeMessagesFromNewProcedure = false)
*/
function info($msg, $immediately = false)
{
return alert('info', $msg, $immediately);
return alert(NotificationType::INFORMATION, $msg, $immediately);
}
/**
@ -58,7 +33,7 @@ function info($msg, $immediately = false)
*/
function warning($msg, $immediately = false)
{
return alert('warning', $msg, $immediately);
return alert(NotificationType::WARNING, $msg, $immediately);
}
/**
@ -70,7 +45,7 @@ function warning($msg, $immediately = false)
*/
function error($msg, $immediately = false)
{
return alert('danger', $msg, $immediately);
return alert(NotificationType::ERROR, $msg, $immediately);
}
/**
@ -82,31 +57,44 @@ function error($msg, $immediately = false)
*/
function success($msg, $immediately = false)
{
return alert('success', $msg, $immediately);
return alert(NotificationType::MESSAGE, $msg, $immediately);
}
/**
* Renders an alert message with the given alert-* class.
* Renders an alert message with the given alert-* class or sets it in session
*
* @param string $class
* @param string $msg
* @param bool $immediately
* @see \Engelsystem\Controllers\HasUserNotifications
*
* @param NotificationType $type
* @param string $msg
* @param bool $immediately
* @return string
*/
function alert($class, $msg, $immediately = false)
function alert(NotificationType $type, $msg, $immediately = false)
{
if (empty($msg)) {
return '';
}
if ($immediately) {
return '<div class="alert alert-' . $class . '" role="alert">' . $msg . '</div>';
$type = str_replace(
[
NotificationType::ERROR->value,
NotificationType::WARNING->value,
NotificationType::INFORMATION->value,
NotificationType::MESSAGE->value,
],
['danger', 'warning', 'info', 'success'],
$type->value
);
return '<div class="alert alert-' . $type . '" role="alert">' . $msg . '</div>';
}
$type = 'messages.' . $type->value;
$session = session();
$message = $session->get('msg', '');
$message .= alert($class, $msg, true);
$session->set('msg', $message);
$messages = $session->get($type, []);
$messages[] = $msg;
$session->set($type, $messages);
return '';
}

View File

@ -50,6 +50,7 @@ function admin_shifts()
}
// Load angeltypes
/** @var AngelType[] $types */
$types = AngelType::all();
$needed_angel_types = [];
foreach ($types as $type) {

View File

@ -121,7 +121,7 @@ function guest_register()
}
if (User::whereName($nick)->count() > 0) {
$valid = false;
$msg .= error(sprintf(__('Your nick &quot;%s&quot; already exists.'), $nick), true);
$msg .= error(sprintf(__('Your nick "%s" already exists.'), htmlspecialchars($nick)), true);
}
} else {
$valid = false;
@ -330,8 +330,8 @@ function guest_register()
}
// If a welcome message is present, display it on the next page
if ($message = $config->get('welcome_msg')) {
info((new Parsedown())->text($message));
if ($config->get('welcome_msg')) {
$session->set('show_welcome', true);
}
// Login the user

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Engelsystem\Controllers\Admin\Schedule;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Helpers\Carbon;
use DateTimeInterface;
use Engelsystem\Controllers\BaseController;
@ -83,7 +84,7 @@ class ImportSchedule extends BaseController
[
'is_index' => true,
'schedules' => ScheduleUrl::all(),
] + $this->getNotifications()
]
);
}
@ -98,7 +99,7 @@ class ImportSchedule extends BaseController
[
'schedule' => $schedule,
'shift_types' => ShiftType::all()->pluck('name', 'id'),
] + $this->getNotifications()
]
);
}
@ -169,7 +170,7 @@ class ImportSchedule extends BaseController
$schedule
) = $this->getScheduleData($request);
} catch (ErrorException $e) {
$this->addNotification($e->getMessage(), 'errors');
$this->addNotification($e->getMessage(), NotificationType::ERROR);
return back();
}
@ -186,7 +187,7 @@ class ImportSchedule extends BaseController
'update' => $changeEvents,
'delete' => $deleteEvents,
],
] + $this->getNotifications()
]
);
}
@ -210,7 +211,7 @@ class ImportSchedule extends BaseController
$scheduleUrl
) = $this->getScheduleData($request);
} catch (ErrorException $e) {
$this->addNotification($e->getMessage(), 'errors');
$this->addNotification($e->getMessage(), NotificationType::ERROR);
return back();
}
@ -250,8 +251,8 @@ class ImportSchedule extends BaseController
$scheduleUrl->touch();
$this->log('Ended schedule "{name}" import', ['name' => $scheduleUrl->name]);
return redirect($this->url, 303)
->with('messages', ['schedule.import.success']);
$this->addNotification('schedule.import.success');
return redirect($this->url, 303);
}
protected function createRoom(Room $room): void

View File

@ -126,7 +126,7 @@ function ShiftEntry_create_view_supporter(Shift $shift, Room $room, AngelType $a
Shift_view_header($shift, $room),
info(sprintf(
__('Do you want to sign up the following user for this shift as %s?'),
AngelType_name_render($angeltype)
$angeltype->name
), true),
form([
form_select('user_id', __('User'), $users_select, $signup_user->id),
@ -153,7 +153,7 @@ function ShiftEntry_create_view_user(Shift $shift, Room $room, AngelType $angelt
. ' <small title="' . $start . '" data-countdown-ts="' . $shift->start->timestamp . '">%c</small>',
[
Shift_view_header($shift, $room),
info(sprintf(__('Do you want to sign up for this shift as %s?'), AngelType_name_render($angeltype)), true),
info(sprintf(__('Do you want to sign up for this shift as %s?'), $angeltype->name), true),
form([
form_textarea('comment', __('Comment (for your eyes only):'), $comment),
form_submit('submit', icon('check-lg') . __('Save')),

View File

@ -20,7 +20,7 @@ function UserAngelType_update_view(UserAngelType $user_angeltype, User $user, An
? __('Do you really want to add supporter rights for %s to %s?')
: __('Do you really want to remove supporter rights for %s from %s?'),
$angeltype->name,
User_Nick_render($user)
$user->name
), true),
form([
buttons([
@ -92,7 +92,7 @@ function UserAngelType_confirm_view(UserAngelType $user_angeltype, User $user, A
msg(),
info(sprintf(
__('Do you really want to confirm %s for %s?'),
User_Nick_render($user),
$user->name,
$angeltype->name
), true),
form([
@ -116,7 +116,7 @@ function UserAngelType_delete_view(UserAngelType $user_angeltype, User $user, An
msg(),
info(sprintf(
__('Do you really want to delete %s from %s?'),
User_Nick_render($user),
$user->name,
$angeltype->name
), true),
form([
@ -170,7 +170,7 @@ function UserAngelType_join_view($user, AngelType $angeltype)
msg(),
info(sprintf(
__('Do you really want to add %s to %s?'),
User_Nick_render($user),
$user->name,
$angeltype->name
), true),
form([

View File

@ -534,7 +534,7 @@ function User_view(
. htmlspecialchars($user_source->name)
. (config('enable_user_name') ? ' <small>' . $user_name . '</small>' : ''),
[
msg(true),
msg(),
div('row', [
div('col-md-12', [
buttons([

View File

@ -1545,8 +1545,8 @@ msgstr ""
#: includes/pages/guest_login.php:82
#, php-format
msgid "Your nick &quot;%s&quot; already exists."
msgstr "Der Nick &quot;%s&quot; existiert schon."
msgid "Your nick \"%s\" already exists."
msgstr "Der Nick \"%s\" existiert bereits."
#: includes/pages/guest_login.php:86
msgid "Please enter a nickname."

View File

@ -1296,8 +1296,8 @@ msgstr "Logout"
#: includes/pages/guest_login.php:56
#, php-format
msgid "Your nick &quot;%s&quot; already exists."
msgstr "Seu apelido &quot;%s&quot; já existe."
msgid "Your nick \"%s\" already exists."
msgstr "Seu apelido \"%s\" já existe."
#: includes/pages/guest_login.php:60
#, php-format

View File

@ -1,19 +1,17 @@
{% import 'macros/base.twig' as m %}
{{ msg() }}
{% for message in errors|default([]) %}
{% for message in notifications('error') %}
{{ m.alert(__(message), 'danger') }}
{% endfor %}
{% for message in warnings|default([]) %}
{% for message in notifications('warning') %}
{{ m.alert(__(message), 'warning') }}
{% endfor %}
{% for message in information|default([]) %}
{% for message in notifications('information') %}
{{ m.alert(__(message), 'info') }}
{% endfor %}
{% for message in messages|default([]) %}
{% for message in notifications('message') %}
{{ m.alert(__(message), 'success') }}
{% endfor %}

View File

@ -34,6 +34,10 @@
<div class="card-body">
{% include 'layouts/parts/messages.twig' %}
{% if session_get('show_welcome', false) %}
{{ m.alert(config('welcome_msg') | md, null, true) }}
{% endif %}
<form action="" enctype="multipart/form-data" method="post">
{{ csrf() }}
<div class="mb-3">

View File

@ -83,7 +83,7 @@ class FaqController extends BaseController
{
return $this->response->withView(
'pages/faq/edit.twig',
['faq' => $faq] + $this->getNotifications()
['faq' => $faq]
);
}
}

View File

@ -49,7 +49,7 @@ class NewsController extends BaseController
'news' => $news,
'is_meeting' => $news ? $news->is_meeting : $isMeetingDefault,
'is_pinned' => $news ? $news->is_pinned : false,
] + $this->getNotifications(),
],
);
}

View File

@ -42,7 +42,7 @@ class QuestionsController extends BaseController
return $this->response->withView(
'pages/questions/overview.twig',
['questions' => $questions, 'is_admin' => true] + $this->getNotifications()
['questions' => $questions, 'is_admin' => true]
);
}
@ -120,7 +120,7 @@ class QuestionsController extends BaseController
{
return $this->response->withView(
'pages/questions/edit.twig',
['question' => $question, 'is_admin' => true] + $this->getNotifications()
['question' => $question, 'is_admin' => true]
);
}
}

View File

@ -42,7 +42,7 @@ class UserShirtController extends BaseController
return $this->response->withView(
'admin/user/edit-shirt.twig',
['userdata' => $user] + $this->getNotifications()
['userdata' => $user]
);
}

View File

@ -142,7 +142,7 @@ class UserWorkLogController extends BaseController
'work_hours' => $work_hours,
'comment' => $comment,
'is_edit' => $is_edit,
] + $this->getNotifications()
]
);
}

View File

@ -39,10 +39,7 @@ class AuthController extends BaseController
protected function showLogin(): Response
{
return $this->response->withView(
'pages/login',
$this->getNotifications()
);
return $this->response->withView('pages/login');
}
/**
@ -58,7 +55,7 @@ class AuthController extends BaseController
$user = $this->auth->authenticate($data['login'], $data['password']);
if (!$user instanceof User) {
$this->addNotification('auth.not-found', 'errors');
$this->addNotification('auth.not-found', NotificationType::ERROR);
return $this->showLogin();
}

View File

@ -32,7 +32,7 @@ class FaqController extends BaseController
return $this->response->withView(
'pages/faq/overview.twig',
['text' => $text, 'items' => $faq] + $this->getNotifications()
['text' => $text, 'items' => $faq]
);
}
}

View File

@ -4,25 +4,40 @@ declare(strict_types=1);
namespace Engelsystem\Controllers;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
trait HasUserNotifications
{
protected function addNotification(string|array $value, string $type = 'messages'): void
protected function addNotification(string|array $value, NotificationType $type = NotificationType::MESSAGE): void
{
$type = 'messages.' . $type->value;
session()->set(
$type,
array_merge(session()->get($type, []), [$value])
array_merge_recursive(session()->get($type, []), (array) $value)
);
}
protected function getNotifications(): array
/**
* @param NotificationType[]|null $types
* @return array<string,Collection|array<string>>
*/
protected function getNotifications(array $types = null): array
{
$return = [];
foreach (['errors', 'warnings', 'information', 'messages'] as $type) {
$return[$type] = Collection::make(Arr::flatten(session()->get($type, [])));
session()->remove($type);
$types = $types ?: [
NotificationType::ERROR,
NotificationType::WARNING,
NotificationType::INFORMATION,
NotificationType::MESSAGE,
];
foreach ($types as $type) {
$type = $type->value;
$path = 'messages.' . $type;
$return[$type] = Collection::make(
session()->get($path, [])
)->flatten();
session()->remove($path);
}
return $return;

View File

@ -157,8 +157,6 @@ class NewsController extends BaseController
*/
protected function renderView(string $page, array $data): Response
{
$data += $this->getNotifications();
return $this->response->withView($page, $data);
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Controllers;
enum NotificationType: string
{
case ERROR = 'error';
case WARNING = 'warning';
case INFORMATION = 'information';
case MESSAGE = 'message';
}

View File

@ -88,7 +88,7 @@ class PasswordResetController extends BaseController
]);
if ($data['password'] !== $data['password_confirmation']) {
$this->addNotification('validation.password.confirmed', 'errors');
$this->addNotification('validation.password.confirmed', NotificationType::ERROR);
return $this->showView('pages/password/reset-form');
}
@ -101,10 +101,7 @@ class PasswordResetController extends BaseController
protected function showView(string $view = 'pages/password/reset', array $data = []): Response
{
return $this->response->withView(
$view,
array_merge_recursive($this->getNotifications(), $data)
);
return $this->response->withView($view, $data);
}
protected function requireToken(Request $request): PasswordReset

View File

@ -40,7 +40,7 @@ class QuestionsController extends BaseController
return $this->response->withView(
'pages/questions/overview.twig',
['questions' => $questions] + $this->getNotifications()
['questions' => $questions]
);
}
@ -48,7 +48,7 @@ class QuestionsController extends BaseController
{
return $this->response->withView(
'pages/questions/edit.twig',
['question' => null] + $this->getNotifications()
['question' => null]
);
}

View File

@ -40,7 +40,7 @@ class SettingsController extends BaseController
[
'settings_menu' => $this->settingsMenu(),
'user' => $user,
] + $this->getNotifications()
]
);
}
@ -60,10 +60,10 @@ class SettingsController extends BaseController
if (config('enable_planned_arrival')) {
if (!$this->isArrivalDateValid($data['planned_arrival_date'], $data['planned_departure_date'])) {
$this->addNotification('settings.profile.planned_arrival_date.invalid', 'errors');
$this->addNotification('settings.profile.planned_arrival_date.invalid', NotificationType::ERROR);
return $this->redirect->to('/settings/profile');
} elseif (!$this->isDepartureDateValid($data['planned_arrival_date'], $data['planned_departure_date'])) {
$this->addNotification('settings.profile.planned_departure_date.invalid', 'errors');
$this->addNotification('settings.profile.planned_departure_date.invalid', NotificationType::ERROR);
return $this->redirect->to('/settings/profile');
} else {
$user->personalData->planned_arrival_date = $data['planned_arrival_date'];
@ -115,7 +115,7 @@ class SettingsController extends BaseController
[
'settings_menu' => $this->settingsMenu(),
'min_length' => config('min_password_length'),
] + $this->getNotifications()
]
);
}
@ -131,9 +131,9 @@ class SettingsController extends BaseController
]);
if (!empty($user->password) && !$this->auth->verifyPassword($user, $data['password'])) {
$this->addNotification('auth.password.error', 'errors');
$this->addNotification('auth.password.error', NotificationType::ERROR);
} elseif ($data['new_password'] != $data['new_password2']) {
$this->addNotification('validation.password.confirmed', 'errors');
$this->addNotification('validation.password.confirmed', NotificationType::ERROR);
} else {
$this->auth->setPassword($user, $data['new_password']);
@ -158,7 +158,7 @@ class SettingsController extends BaseController
'settings_menu' => $this->settingsMenu(),
'themes' => $themes,
'current_theme' => $currentTheme,
] + $this->getNotifications()
]
);
}
@ -192,7 +192,7 @@ class SettingsController extends BaseController
'settings_menu' => $this->settingsMenu(),
'languages' => $languages,
'current_language' => $currentLanguage,
] + $this->getNotifications()
]
);
}
@ -228,7 +228,7 @@ class SettingsController extends BaseController
[
'settings_menu' => $this->settingsMenu(),
'providers' => $providers,
] + $this->getNotifications(),
],
);
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Engelsystem\Middleware;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Http\Exceptions\HttpException;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Request;
@ -55,7 +56,10 @@ class ErrorHandler implements MiddlewareInterface
$response = $this->createResponse($e->getMessage(), $e->getStatusCode(), $e->getHeaders());
} catch (ValidationException $e) {
$response = $this->redirectBack();
$response->with('errors', ['validation' => $e->getValidator()->getErrors()]);
$response->with(
'messages.' . NotificationType::ERROR->value,
['validation' => $e->getValidator()->getErrors()]
);
if ($request instanceof Request) {
$response->withInput(Arr::except($request->request->all(), $this->formIgnore));

View File

@ -26,7 +26,6 @@ class Legacy extends TwigExtension
new TwigFunction('menuUserHints', 'header_render_hints', $isSafeHtml),
new TwigFunction('menuLanguages', 'make_language_select', $isSafeHtml),
new TwigFunction('page', [$this, 'getPage']),
new TwigFunction('msg', 'msg', $isSafeHtml),
];
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Renderer\Twig\Extensions;
use Engelsystem\Controllers\HasUserNotifications;
use Engelsystem\Controllers\NotificationType;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\Session\Session as SymfonySession;
use Twig\Extension\AbstractExtension as TwigExtension;
use Twig\TwigFunction;
class Notification extends TwigExtension
{
use HasUserNotifications;
public function __construct(protected SymfonySession $session)
{
}
/**
* @return TwigFunction[]
*/
public function getFunctions(): array
{
return [
new TwigFunction('notifications', [$this, 'notifications']),
];
}
/**
* @return Collection|Collection[]
*/
public function notifications(string $type = null): Collection
{
$types = $type ? [NotificationType::from($type)] : null;
$messages = $this->getNotifications($types);
if ($types) {
$messages = $messages[$type] ?? [];
}
return collect($messages);
}
}

View File

@ -14,6 +14,7 @@ use Engelsystem\Renderer\Twig\Extensions\Develop;
use Engelsystem\Renderer\Twig\Extensions\Globals;
use Engelsystem\Renderer\Twig\Extensions\Legacy;
use Engelsystem\Renderer\Twig\Extensions\Markdown;
use Engelsystem\Renderer\Twig\Extensions\Notification;
use Engelsystem\Renderer\Twig\Extensions\Session;
use Engelsystem\Renderer\Twig\Extensions\Translation;
use Engelsystem\Renderer\Twig\Extensions\Url;
@ -34,6 +35,7 @@ class TwigServiceProvider extends ServiceProvider
'csrf' => Csrf::class,
'develop' => Develop::class,
'globals' => Globals::class,
'notification' => Notification::class,
'twigmodel' => TwigModel::class,
'session' => Session::class,
'legacy' => Legacy::class,

View File

@ -5,12 +5,11 @@ declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers\Admin;
use Engelsystem\Controllers\Admin\FaqController;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\Faq;
use Engelsystem\Test\Unit\Controllers\ControllerTest;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\Session\Session;
class FaqControllerTest extends ControllerTest
{
@ -33,10 +32,7 @@ class FaqControllerTest extends ControllerTest
->willReturnCallback(function ($view, $data) {
$this->assertEquals('pages/faq/edit.twig', $view);
/** @var Collection $warnings */
$warnings = $data['messages'];
$this->assertNotEmpty($data['faq']);
$this->assertTrue($warnings->isEmpty());
return $this->response;
});
@ -45,6 +41,7 @@ class FaqControllerTest extends ControllerTest
$controller = $this->app->make(FaqController::class);
$controller->edit($this->request);
$this->assertHasNoNotifications(NotificationType::WARNING);
}
/**
@ -82,14 +79,10 @@ class FaqControllerTest extends ControllerTest
$this->assertTrue($this->log->hasInfoThatContains('Updated'));
/** @var Session $session */
$session = $this->app->get('session');
$messages = $session->get('messages');
$this->assertEquals('faq.edit.success', $messages[0]);
$faq = (new Faq())->find(2);
$this->assertEquals('Foo?', $faq->question);
$this->assertEquals('Bar!', $faq->text);
$this->assertHasNotification('faq.edit.success');
}
/**
@ -153,10 +146,7 @@ class FaqControllerTest extends ControllerTest
$this->assertTrue($this->log->hasInfoThatContains('Deleted'));
/** @var Session $session */
$session = $this->app->get('session');
$messages = $session->get('messages');
$this->assertEquals('faq.delete.success', $messages[0]);
$this->assertHasNotification('faq.delete.success');
}
/**

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers\Admin;
use Engelsystem\Controllers\Admin\NewsController;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Events\EventDispatcher;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\ValidationException;
@ -12,9 +13,7 @@ use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\News;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\Controllers\ControllerTest;
use Illuminate\Support\Collection;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\HttpFoundation\Session\Session;
class NewsControllerTest extends ControllerTest
{
@ -42,10 +41,7 @@ class NewsControllerTest extends ControllerTest
->willReturnCallback(function ($view, $data) {
$this->assertEquals('pages/news/edit.twig', $view);
/** @var Collection $warnings */
$warnings = $data['warnings'];
$this->assertNotEmpty($data['news']);
$this->assertTrue($warnings->isEmpty());
return $this->response;
});
@ -54,6 +50,7 @@ class NewsControllerTest extends ControllerTest
$controller = $this->app->make(NewsController::class);
$controller->edit($this->request);
$this->assertHasNoNotifications(NotificationType::WARNING);
}
/**
@ -147,10 +144,7 @@ class NewsControllerTest extends ControllerTest
$this->assertTrue($this->log->hasInfoThatContains('Updated'));
/** @var Session $session */
$session = $this->app->get('session');
$messages = $session->get('messages');
$this->assertEquals('news.edit.success', $messages[0]);
$this->assertHasNotification('news.edit.success');
$news = (new News())->find($id);
$this->assertEquals($text, $news->text);
@ -224,10 +218,7 @@ class NewsControllerTest extends ControllerTest
$this->assertTrue($this->log->hasInfoThatContains('Deleted'));
/** @var Session $session */
$session = $this->app->get('session');
$messages = $session->get('messages');
$this->assertEquals('news.delete.success', $messages[0]);
$this->assertHasNotification('news.delete.success');
}
/**

View File

@ -7,6 +7,7 @@ namespace Engelsystem\Test\Unit\Controllers;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\AuthController;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Redirector;
@ -16,13 +17,12 @@ use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
class AuthControllerTest extends TestCase
class AuthControllerTest extends ControllerTest
{
use ArraySubsetAsserts;
use HasDatabase;
@ -42,11 +42,6 @@ class AuthControllerTest extends TestCase
/** @var Authenticator|MockObject $auth */
list(, $session, $redirect, $config, $auth) = $this->getMocks();
$session->expects($this->atLeastOnce())
->method('get')
->willReturnCallback(function ($type) {
return $type == 'errors' ? ['foo' => 'bar'] : [];
});
$response->expects($this->once())
->method('withView')
->with('pages/login')
@ -70,11 +65,10 @@ class AuthControllerTest extends TestCase
/** @var Config $config */
/** @var Authenticator|MockObject $auth */
list(, , $redirect, $config, $auth) = $this->getMocks();
$session = new Session(new MockArraySessionStorage());
$this->session = new Session(new MockArraySessionStorage());
$this->app->instance('session', $this->session);
/** @var Validator|MockObject $validator */
$validator = new Validator();
$session->set('errors', [['bar' => 'some.bar.error']]);
$this->app->instance('session', $session);
$user = $this->createUser();
$auth->expects($this->exactly(2))
@ -86,13 +80,12 @@ class AuthControllerTest extends TestCase
->method('withView')
->willReturnCallback(function ($view, $data = []) use ($response) {
$this->assertEquals('pages/login', $view);
$this->assertArraySubset(['errors' => collect(['some.bar.error', 'auth.not-found'])], $data);
return $response;
});
/** @var AuthController|MockObject $controller */
$controller = $this->getMockBuilder(AuthController::class)
->setConstructorArgs([$response, $session, $redirect, $config, $auth])
->setConstructorArgs([$response, $this->session, $redirect, $config, $auth])
->onlyMethods(['loginUser'])
->getMock();
$controller->setValidator($validator);
@ -120,7 +113,7 @@ class AuthControllerTest extends TestCase
// No user found
$request = new Request([], ['login' => 'foo', 'password' => 'bar']);
$controller->postLogin($request);
$this->assertEquals([], $session->all());
$this->assertHasNotification('auth.not-found', NotificationType::ERROR);
// Authenticated user
$controller->postLogin($request);

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Http\UrlGenerator;
@ -33,12 +34,26 @@ abstract class ControllerTest extends TestCase
protected Session $session;
/**
* @param string|null $type
* @param string|string[] $value
*/
protected function assertHasNotification(string $value, string $type = 'messages'): void
protected function setNotification(string|array $value, NotificationType $type = NotificationType::MESSAGE): void
{
$messages = $this->session->get($type, []);
$this->assertTrue(in_array($value, $messages));
$this->session->set(
'messages.' . $type->value,
array_merge($this->session->get('messages.' . $type->value, []), (array) $value)
);
}
protected function assertHasNotification(string $value, NotificationType $type = NotificationType::MESSAGE): void
{
$messages = $this->session->get('messages.' . $type->value, []);
$this->assertTrue(in_array($value, $messages), 'Has ' . $type->value . ' notification: ' . $value);
}
protected function assertHasNoNotifications(NotificationType $type = null): void
{
$messages = $this->session->get('messages' . ($type ? '.' . $type->value : ''), []);
$this->assertEmpty($messages, 'Has no' . ($type ? ' ' . $type->value : '') . ' notification.');
}
/**

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Test\Unit\Controllers\Stub\HasUserNotificationsImplementation;
use Engelsystem\Test\Unit\TestCase;
use Illuminate\Support\Collection;
@ -22,16 +23,17 @@ class HasUserNotificationsTest extends TestCase
$this->app->instance('session', $session);
$notify = new HasUserNotificationsImplementation();
$notify->add('Foo', 'errors');
$notify->add('Bar', 'warnings');
$notify->add(['Baz', 'Lorem'], 'information');
$notify->add(['Hm', ['Uff', 'sum']], 'messages');
$notify->add('Foo', NotificationType::ERROR);
$notify->add('Bar', NotificationType::WARNING);
$notify->add(['Baz', 'Lorem'], NotificationType::INFORMATION);
$notify->add(['Hm', ['test'], 'some' => ['Uff', 'sum']], NotificationType::MESSAGE);
$notify->add(['some' => ['it']], NotificationType::MESSAGE);
$this->assertEquals([
'errors' => new Collection(['Foo']),
'warnings' => new Collection(['Bar']),
'information' => new Collection(['Baz', 'Lorem']),
'messages' => new Collection(['Hm', 'Uff', 'sum']),
NotificationType::ERROR->value => new Collection(['Foo']),
NotificationType::WARNING->value => new Collection(['Bar']),
NotificationType::INFORMATION->value => new Collection(['Baz', 'Lorem']),
NotificationType::MESSAGE->value => new Collection(['Hm', 'test', 'Uff', 'sum', 'it']),
], $notify->get());
}
}

View File

@ -6,6 +6,7 @@ namespace Engelsystem\Test\Unit\Controllers;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Controllers\PasswordResetController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\HttpNotFound;
@ -18,13 +19,12 @@ use Engelsystem\Models\User\PasswordReset;
use Engelsystem\Models\User\User;
use Engelsystem\Renderer\Renderer;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\Test\TestLogger;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
class PasswordResetControllerTest extends TestCase
class PasswordResetControllerTest extends ControllerTest
{
use ArraySubsetAsserts;
use HasDatabase;
@ -55,7 +55,7 @@ class PasswordResetControllerTest extends TestCase
$controller = $this->getController(
'pages/password/reset-success',
['type' => 'email', 'errors' => collect()]
['type' => 'email']
);
/** @var TestLogger $log */
$log = $this->args['log'];
@ -67,6 +67,7 @@ class PasswordResetControllerTest extends TestCase
$this->assertNotEmpty((new PasswordReset())->find($user->id)->first());
$this->assertTrue($log->hasInfoThatContains($user->name));
$this->assertHasNoNotifications();
}
/**
@ -92,10 +93,11 @@ class PasswordResetControllerTest extends TestCase
$controller = $this->getController(
'pages/password/reset-success',
['type' => 'email', 'errors' => collect()]
['type' => 'email']
);
$controller->postReset($request);
$this->assertHasNoNotifications();
}
/**
@ -148,7 +150,7 @@ class PasswordResetControllerTest extends TestCase
$controller = $this->getController(
'pages/password/reset-success',
['type' => 'reset', 'errors' => collect()]
['type' => 'reset']
);
$auth = new Authenticator($request, $this->args['session'], $user);
@ -159,6 +161,7 @@ class PasswordResetControllerTest extends TestCase
$this->assertEmpty((new PasswordReset())->find($user->id));
$this->assertNotNull(auth()->authenticate($user->name, $password));
$this->assertHasNoNotifications();
}
/**
@ -179,16 +182,10 @@ class PasswordResetControllerTest extends TestCase
['token' => $token->token]
);
$controller = $this->getController(
'pages/password/reset-form',
['errors' => collect(['some.other.error', 'validation.password.confirmed'])]
);
/** @var Session $session */
$session = $this->args['session'];
$session->set('errors', ['foo' => ['bar' => 'some.other.error']]);
$controller = $this->getController('pages/password/reset-form');
$controller->postResetPassword($request);
$this->assertEmpty($session->get('errors'));
$this->assertHasNotification('validation.password.confirmed', NotificationType::ERROR);
}
protected function getControllerArgs(): array
@ -203,6 +200,10 @@ class PasswordResetControllerTest extends TestCase
$this->app->instance('session', $session);
$this->session = $session;
$this->response = $response;
$this->log = $log;
return $this->args = [
'response' => $response,
'session' => $session,

View File

@ -6,6 +6,7 @@ namespace Engelsystem\Test\Unit\Controllers;
use Carbon\Carbon;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Controllers\SettingsController;
use Engelsystem\Http\Exceptions\HttpNotFound;
use Engelsystem\Http\Response;
@ -133,7 +134,7 @@ class SettingsControllerTest extends ControllerTest
$this->setUpProfileTest();
config(['buildup_start' => new Carbon('2022-01-02')]); // arrival before buildup
$this->controller->saveProfile($this->request);
$this->assertHasNotification('settings.profile.planned_arrival_date.invalid', 'errors');
$this->assertHasNotification('settings.profile.planned_arrival_date.invalid', NotificationType::ERROR);
}
/**
@ -144,7 +145,7 @@ class SettingsControllerTest extends ControllerTest
$this->setUpProfileTest();
config(['teardown_end' => new Carbon('2022-01-01')]); // departure after teardown
$this->controller->saveProfile($this->request);
$this->assertHasNotification('settings.profile.planned_departure_date.invalid', 'errors');
$this->assertHasNotification('settings.profile.planned_departure_date.invalid', NotificationType::ERROR);
}
/**
@ -272,7 +273,7 @@ class SettingsControllerTest extends ControllerTest
/** @var Session $session */
$session = $this->app->get('session');
$messages = $session->get('messages');
$messages = $session->get('messages.' . NotificationType::MESSAGE->value);
$this->assertEquals('settings.password.success', $messages[0]);
}
@ -328,10 +329,7 @@ class SettingsControllerTest extends ControllerTest
$this->controller->savePassword($this->request);
/** @var Session $session */
$session = $this->app->get('session');
$errors = $session->get('errors');
$this->assertEquals('auth.password.error', $errors[0]);
$this->assertHasNotification('auth.password.error', NotificationType::ERROR);
}
/**
@ -359,10 +357,7 @@ class SettingsControllerTest extends ControllerTest
$this->controller->savePassword($this->request);
/** @var Session $session */
$session = $this->app->get('session');
$errors = $session->get('errors');
$this->assertEquals('validation.password.confirmed', $errors[0]);
$this->assertHasNotification('validation.password.confirmed', NotificationType::ERROR);
}
public function savePasswordValidationProvider(): array
@ -558,7 +553,6 @@ class SettingsControllerTest extends ControllerTest
->method('withView')
->willReturnCallback(function ($view, $data) use ($providers) {
$this->assertEquals('pages/settings/oauth', $view);
$this->assertArrayHasKey('information', $data);
$this->assertArrayHasKey('providers', $data);
$this->assertEquals($providers, $data['providers']);

View File

@ -5,12 +5,13 @@ declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers\Stub;
use Engelsystem\Controllers\HasUserNotifications;
use Engelsystem\Controllers\NotificationType;
class HasUserNotificationsImplementation
{
use HasUserNotifications;
public function add(string|array $value, string $type = 'messages'): void
public function add(string|array $value, NotificationType $type = NotificationType::MESSAGE): void
{
$this->addNotification($value, $type);
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Engelsystem\Test\Unit\Middleware;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Http\Exceptions\HttpException;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Psr7ServiceProvider;
@ -185,7 +186,10 @@ class ErrorHandlerTest extends TestCase
->willReturn(['foo' => ['validation.foo.numeric']]);
$session = new Session(new MockArraySessionStorage());
$session->set('errors', ['validation' => ['foo' => ['validation.foo.required']]]);
$session->set(
'messages.' . NotificationType::ERROR->value,
['validation' => ['foo' => ['validation.foo.required']]]
);
$request = Request::create(
'/foo/bar',
'POST',
@ -208,7 +212,7 @@ class ErrorHandlerTest extends TestCase
$this->assertEquals(302, $return->getStatusCode());
$this->assertEquals('http://localhost/', $return->getHeaderLine('location'));
$this->assertEquals([
'errors' => [
'messages.' . NotificationType::ERROR->value => [
'validation' => [
'foo' => [
'validation.foo.required',

View File

@ -27,7 +27,6 @@ class LegacyTest extends ExtensionTest
$this->assertExtensionExists('menuUserHints', 'header_render_hints', $functions, $isSafeHtml);
$this->assertExtensionExists('menuLanguages', 'make_language_select', $functions, $isSafeHtml);
$this->assertExtensionExists('page', [$extension, 'getPage'], $functions);
$this->assertExtensionExists('msg', 'msg', $functions, $isSafeHtml);
}
/**

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Renderer\Twig\Extensions\Notification;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
class NotificationTest extends ExtensionTest
{
/**
* @covers \Engelsystem\Renderer\Twig\Extensions\Notification::__construct
* @covers \Engelsystem\Renderer\Twig\Extensions\Notification::getFunctions
*/
public function testGetFunctions(): void
{
$session = new Session(new MockArraySessionStorage());
$extension = new Notification($session);
$functions = $extension->getFunctions();
$this->assertExtensionExists('notifications', [$extension, 'notifications'], $functions);
}
/**
* @covers \Engelsystem\Renderer\Twig\Extensions\Notification::notifications
*/
public function testNotifications(): void
{
$session = new Session(new MockArraySessionStorage());
$extension = new Notification($session);
$this->app->instance('session', $session);
$notificationsList = $extension->notifications()->toArray();
$this->assertIsArray($notificationsList);
foreach ($notificationsList as $notification) {
$this->assertEmpty($notification);
}
$session->set('messages.' . NotificationType::ERROR->value, 'some error');
$session->set('messages.' . NotificationType::WARNING->value, 'a warning');
$session->set('messages.' . NotificationType::INFORMATION->value, 'for your information');
$session->set('messages.' . NotificationType::MESSAGE->value, 'i\'m a message');
$notifications = $extension->notifications();
$this->assertEquals(['some error'], $notifications[NotificationType::ERROR->value]->toArray());
$this->assertEquals(['a warning'], $notifications[NotificationType::WARNING->value]->toArray());
$this->assertEquals(['for your information'], $notifications[NotificationType::INFORMATION->value]->toArray());
$this->assertEquals(['i\'m a message'], $notifications[NotificationType::MESSAGE->value]->toArray());
$session->set('messages.' . NotificationType::ERROR->value, 'Test error');
$notifications = $extension->notifications(NotificationType::ERROR->value);
$this->assertEquals(['Test error'], $notifications->toArray());
}
}