Settings Modernization: Applying buildup and teardown time, add localization

This commit is contained in:
frischler 2022-10-13 20:23:22 +02:00 committed by Igor Scheller
parent 203531629f
commit d6899d37d9
14 changed files with 384 additions and 31 deletions

View File

@ -110,6 +110,17 @@ msgstr "OAuth-Provider nicht gefunden"
msgid "settings.profile" msgid "settings.profile"
msgstr "Profil" msgstr "Profil"
msgid "settings.profile.planned_arrival_date.invalid"
msgstr "Bitte gib Dein geplantes Ankunftsdatum an. "
"Es sollte nach dem Aufbaubeginn und vor dem Abbauende liegen."
msgid "settings.profile.planned_departure_date.invalid"
msgstr "Bitte gib Dein geplantes Abreisedatum an, damit wir ein Gefühl für die Abbauplanung bekommen. "
"Es sollte nach dem Aufbaubeginn und vor dem Abbauende liegen."
msgid "settings.profile.success"
msgstr "Einstellungen gespeichert."
msgid "faq.delete.success" msgid "faq.delete.success"
msgstr "FAQ Eintrag erfolgreich gelöscht." msgstr "FAQ Eintrag erfolgreich gelöscht."

View File

@ -2942,6 +2942,64 @@ msgstr "Nachricht"
msgid "settings.settings" msgid "settings.settings"
msgstr "Einstellungen" msgstr "Einstellungen"
msgid "settings.profile.user_details.info"
msgstr "Hier kannst Du Deine Details ändern."
msgid "settings.profile.entry_required"
msgstr "Pflichtfeld!"
msgid "settings.profile.nick"
msgstr "Nick"
msgid "settings.profile.pronoun"
msgstr "Pronomen"
msgid "settings.profile.pronoun.info"
msgstr "Wird auf deiner Profilseite und in Engellisten angezeigt."
msgid "settings.profile.firstname"
msgstr "Vorname"
msgid "settings.profile.lastname"
msgstr "Nachname"
msgid "settings.profile.planned_arrival_date"
msgstr "Geplanter Ankunftstag"
msgid "settings.profile.planned_departure_date"
msgstr "Geplanter Abreisetag"
msgid "settings.profile.dect"
msgstr "DECT"
msgid "settings.profile.mobile"
msgstr "Handy"
msgid "settings.profile.email"
msgstr "E-Mail"
msgid "settings.profile.email_shiftinfo"
msgstr "Das %s darf mir E-Mails senden (z.B. wenn sich meine Schichten ändern)."
msgid "settings.profile.email_news"
msgstr "Benachrichtige mich bei neuen News."
msgid "settings.profile.email_by_human_allowed"
msgstr "Erlaube Himmel-Engeln dich per Mail zu kontaktieren."
msgid "settings.profile.email_goody"
msgstr "Um Voucher zu erhalten, stimme zu, dass Nick, E-Mail-Adresse, geleistete Arbeit und Shirtgröße "
"bis zum nächsten gleichartigen Event gespeichert werden."
msgid "settings.profile.privacy"
msgstr "Dies kann jederzeit durch eine E-Mail an <a href=\"mailto:%s\">%1$s</a> widerrufen werden."
msgid "settings.profile.shirt_size"
msgstr "T-Shirt-Größe"
msgid "settings.profile.angeltypes.info"
msgstr "Du kannst deine Engeltypen <a href=\"%s\">auf der Engeltypen-Seite</a> verwalten."
msgid "settings.password" msgid "settings.password"
msgstr "Passwort" msgstr "Passwort"

View File

@ -108,6 +108,17 @@ msgstr "Unable to find OAuth provider"
msgid "settings.profile" msgid "settings.profile"
msgstr "Profile" msgstr "Profile"
msgid "settings.profile.planned_arrival_date.invalid"
msgstr "Please enter your planned date of arrival. "
"It should be after the buildup start date and before teardown end date."
msgid "settings.profile.planned_departure_date.invalid"
msgstr "Please enter your planned date of departure. "
"It should be after your planned arrival date and after buildup start date and before teardown end date."
msgid "settings.profile.success"
msgstr "Settings saved."
msgid "faq.delete.success" msgid "faq.delete.success"
msgstr "FAQ entry successfully deleted." msgstr "FAQ entry successfully deleted."

View File

@ -208,6 +208,64 @@ msgstr "Message"
msgid "settings.settings" msgid "settings.settings"
msgstr "Settings" msgstr "Settings"
msgid "settings.profile.user_details.info"
msgstr "Here you can change your user details."
msgid "settings.profile.entry_required"
msgstr "Entry required!"
msgid "settings.profile.nick"
msgstr "Nick"
msgid "settings.profile.pronoun"
msgstr "Pronoun"
msgid "settings.profile.pronoun.info"
msgstr "Will be shown on your profile page and in angel lists."
msgid "settings.profile.firstname"
msgstr "First name"
msgid "settings.profile.lastname"
msgstr "Last name"
msgid "settings.profile.planned_arrival_date"
msgstr "Planned date of arrival"
msgid "settings.profile.planned_departure_date"
msgstr "Planned date of departure"
msgid "settings.profile.dect"
msgstr "DECT"
msgid "settings.profile.mobile"
msgstr "Mobile"
msgid "settings.profile.email"
msgstr "E-Mail"
msgid "settings.profile.email_shiftinfo"
msgstr "The %s is allowed to send me an e-mail (e.g. when my shifts change)."
msgid "settings.profile.email_news"
msgstr "Notify me of new news."
msgid "settings.profile.email_by_human_allowed"
msgstr "Allow heaven angels to contact you by e-mail."
msgid "settings.profile.email_goody"
msgstr "To receive vouchers, give consent that nick, e-mail address, worked hours and shirt size will be stored until "
"the next similar event."
msgid "settings.profile.privacy"
msgstr "To withdraw your approval, send an e-mail to <a href=\"mailto:%s\">%1$s</a>."
msgid "settings.profile.shirt_size"
msgstr "Shirt size"
msgid "settings.profile.angeltypes.info"
msgstr "You can manage your Angeltypes <a href=\"%s\">on the Angeltypes page</a>."
msgid "settings.password" msgid "settings.password"
msgstr "Password" msgstr "Password"

View File

@ -36,8 +36,15 @@
</a> </a>
{% endmacro %} {% endmacro %}
{% macro info(text) %} {% macro info(text, raw) %}
<span class="help-block">{{ _self.icon('info-circle') }}{{ text }}</span> <span class="help-block">
{{ _self.icon('info-circle') }}
{%- if raw|default(false) -%}
{{ text|raw }}
{%- else -%}
{{ text }}
{%- endif -%}
</span>
{%- endmacro %} {%- endmacro %}
{% macro entry_required(text) %} {% macro entry_required(text) %}

View File

@ -12,8 +12,10 @@
type="{{ type|default('text') }}" class="form-control" type="{{ type|default('text') }}" class="form-control"
id="{{ name }}" name="{{ name }}" id="{{ name }}" name="{{ name }}"
value="{{ opt.value|default('')|escape('html_attr') }}" value="{{ opt.value|default('')|escape('html_attr') }}"
{%- if opt.min is defined %} minlength="{{ opt.min }}"{% endif %} {%- if opt.min_length is defined %} minlength="{{ opt.min_length }}"{% endif %}
{%- if opt.max is defined %} maxlength="{{ opt.max }}"{% endif %} {%- if opt.max_length is defined %} maxlength="{{ opt.max_length }}"{% endif %}
{%- if opt.min is defined %} min="{{ opt.min }}"{% endif %}
{%- if opt.max is defined %} max="{{ opt.max }}"{% endif %}
{%- if opt.required|default(false) %} {%- if opt.required|default(false) %}
required required
{%- endif -%} {%- endif -%}
@ -61,14 +63,18 @@
</div> </div>
{%- endmacro %} {%- endmacro %}
{% macro checkbox(name, label, checked, value, disabled) %} {% macro checkbox(name, label, checked, value, disabled, raw_label) %}
<div class="form-check mb-3"> <div class="form-check mb-3">
<label> <label>
<input type="checkbox" id="{{ name }}" name="{{ name }}" value="{{ value|default('1') }}" class="form-check-input" <input type="checkbox" id="{{ name }}" name="{{ name }}" value="{{ value|default('1') }}" class="form-check-input"
{%- if checked|default(false) %} checked{% endif %} {%- if checked|default(false) %} checked{% endif %}
{%- if disabled|default(false) %} disabled{% endif %} {%- if disabled|default(false) %} disabled{% endif %}
> >
{%- if raw_label|default(false) -%}
{{ label|raw }}
{%- else -%}
{{ label }} {{ label }}
{%- endif -%}
</label> </label>
</div> </div>
{%- endmacro %} {%- endmacro %}

View File

@ -7,8 +7,8 @@
<form action="" enctype="multipart/form-data" method="post"> <form action="" enctype="multipart/form-data" method="post">
{{ csrf() }} {{ csrf() }}
{{ f.input('password', __('Password'), 'password', {'min': min_length, 'required': true}) }} {{ f.input('password', __('Password'), 'password', {'min_length': min_length, 'required': true}) }}
{{ f.input('password_confirmation', __('Confirm password'), 'password', {'min': min_length, 'required': true}) }} {{ f.input('password_confirmation', __('Confirm password'), 'password', {'min_length': min_length, 'required': true}) }}
{{ f.submit(__('Save')) }} {{ f.submit(__('Save')) }}
</form> </form>

View File

@ -24,13 +24,13 @@
'new_password', 'new_password',
__('settings.password.new_password'), __('settings.password.new_password'),
'password', 'password',
{'min': min_length, 'required': true} {'min_length': min_length, 'required': true}
) }} ) }}
{{ f.input( {{ f.input(
'new_password2', 'new_password2',
__('settings.password.new_password2'), __('settings.password.new_password2'),
'password', 'password',
{'min': min_length, 'required': true} {'min_length': min_length, 'required': true}
) }} ) }}
{{ f.submit() }} {{ f.submit() }}
</div> </div>

View File

@ -23,7 +23,7 @@
'pronoun', 'pronoun',
__('settings.profile.pronoun'), __('settings.profile.pronoun'),
'text', 'text',
{'value': user.personalData.pronoun ,'max': 15} {'value': user.personalData.pronoun ,'max_length': 15}
) }} ) }}
{{ m.info(__('settings.profile.pronoun.info')) }} {{ m.info(__('settings.profile.pronoun.info')) }}
{% endif %} {% endif %}
@ -32,13 +32,13 @@
'first_name', 'first_name',
__('settings.profile.firstname'), __('settings.profile.firstname'),
'text', 'text',
{'value': user.personalData.first_name, 'max': 64} {'value': user.personalData.first_name, 'max_length': 64}
) }} ) }}
{{ f.input( {{ f.input(
'last_name', 'last_name',
__('settings.profile.lastname'), __('settings.profile.lastname'),
'text', 'text',
{'value': user.personalData.last_name, 'max': 64} {'value': user.personalData.last_name, 'max_length': 64}
) }} ) }}
{% endif %} {% endif %}
</div> </div>
@ -50,16 +50,22 @@
__('settings.profile.planned_arrival_date'), __('settings.profile.planned_arrival_date'),
'date', 'date',
{ {
'value': user.personalData.planned_arrival_date.format(__('Y-m-d')), 'value': user.personalData.planned_arrival_date.format('Y-m-d'),
'required': true, 'required': true,
'entry_required_icon': true 'entry_required_icon': true,
'min': config('buildup_start') ? config('buildup_start').format('Y-m-d') : '',
'max': config('teardown_end') ? config('teardown_end').format('Y-m-d') : '',
} }
) }} ) }}
{{ f.input( {{ f.input(
'planned_departure_date', 'planned_departure_date',
__('settings.profile.planned_departure_date'), __('settings.profile.planned_departure_date'),
'text', 'date',
{'value': user.personalData.planned_departure_date.format(__('Y-m-d'))} {
'value': user.personalData.planned_departure_date.format('Y-m-d'),
'min': config('buildup_start') ? config('buildup_start').format('Y-m-d') : '',
'max': config('teardown_end') ? config('teardown_end').format('Y-m-d') : '',
}
) }} ) }}
</div> </div>
{% endif %} {% endif %}
@ -70,27 +76,27 @@
'dect', 'dect',
__('settings.profile.dect'), __('settings.profile.dect'),
'text', 'text',
{'value': user.contact.dect, 'max': 40} {'value': user.contact.dect, 'max_length': 40}
) }} ) }}
{% endif %} {% endif %}
{{ f.input( {{ f.input(
'mobile', 'mobile',
__('settings.profile.mobile'), __('settings.profile.mobile'),
'text', 'text',
{'value': user.contact.mobile, 'max': 40} {'value': user.contact.mobile, 'max_length': 40}
) }} ) }}
{{ f.input( {{ f.input(
'email', 'email',
__('settings.profile.email'), __('settings.profile.email'),
'email', 'email',
{'value': user.email, 'max': 254, 'required': true, 'entry_required_icon': true} {'value': user.email, 'max_length': 254, 'required': true, 'entry_required_icon': true}
) }} ) }}
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
{{ f.checkbox( {{ f.checkbox(
'email_shiftinfo', 'email_shiftinfo',
__('settings.profile.email_shiftinfo'), __('settings.profile.email_shiftinfo', [config('app_name')]),
user.settings.email_shiftinfo user.settings.email_shiftinfo
) }} ) }}
{{ f.checkbox( {{ f.checkbox(
@ -104,10 +110,17 @@
user.settings.email_human user.settings.email_human
) }} ) }}
{% if config('enable_goody') %} {% if config('enable_goody') %}
{% set privacy_email = config('privacy_email') %}
{% set email_goody_label = __('settings.profile.email_goody') ~
(privacy_email ? ' ' ~ __('settings.profile.privacy', [privacy_email]) : '')
%}
{{ f.checkbox( {{ f.checkbox(
'email_goody', 'email_goody',
__('settings.profile.email_goody'), email_goody_label,
user.settings.email_goody user.settings.email_goody,
user.settings.email_goody,
false,
true
) }} ) }}
{% endif %} {% endif %}
</div> </div>
@ -122,7 +135,7 @@
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
{{ m.info(__('settings.profile.user_details.info')) }} {{ m.info(__('settings.profile.angeltypes.info', [url('/angeltypes')]), true) }}
{{ f.submit() }} {{ f.submit() }}
</div> </div>
</div> </div>

View File

@ -0,0 +1,52 @@
<?php
namespace Engelsystem\Controllers;
use Carbon\Carbon;
use DateTime;
trait ChecksArrivalsAndDepartures
{
protected function isArrivalDateValid(?string $arrival, ?string $departure): bool
{
if (is_null($arrival)) {
return false; // since required value
}
$arrival_carbon = $this->toCarbon($arrival);
if (!is_null($departure) && $arrival_carbon->greaterThan($this->toCarbon($departure))) {
return false;
}
return !$this->isBeforeBuildup($arrival_carbon) && !$this->isAfterTeardown($arrival_carbon);
}
protected function isDepartureDateValid(?string $arrival, ?string $departure): bool
{
if (is_null($departure)) {
return true; // since optional value
}
$departure_carbon = $this->toCarbon($departure);
return $departure_carbon->greaterThanOrEqualTo($this->toCarbon($arrival)) &&
!$this->isBeforeBuildup($departure_carbon) && !$this->isAfterTeardown($departure_carbon);
}
private function toCarbon(string $date_string): Carbon
{
return new Carbon(DateTime::createFromFormat('Y-m-d', $date_string));
}
private function isBeforeBuildup(Carbon $date): bool
{
$buildup = config('buildup_start');
return !empty($buildup) && $date->lessThan($buildup->setTime(0,0));
}
private function isAfterTeardown(Carbon $date): bool
{
$teardown = config('teardown_end');
return !empty($teardown) && $date->greaterThanOrEqualTo($teardown->addDay()->setTime(0,0));
}
}

View File

@ -2,6 +2,7 @@
namespace Engelsystem\Controllers; namespace Engelsystem\Controllers;
use Carbon\Carbon;
use Engelsystem\Config\Config; use Engelsystem\Config\Config;
use Engelsystem\Http\Exceptions\HttpNotFound; use Engelsystem\Http\Exceptions\HttpNotFound;
use Engelsystem\Http\Response; use Engelsystem\Http\Response;
@ -13,6 +14,7 @@ use Psr\Log\LoggerInterface;
class SettingsController extends BaseController class SettingsController extends BaseController
{ {
use HasUserNotifications; use HasUserNotifications;
use ChecksArrivalsAndDepartures;
/** @var Authenticator */ /** @var Authenticator */
protected $auth; protected $auth;
@ -72,6 +74,10 @@ class SettingsController extends BaseController
public function saveProfile(Request $request): Response public function saveProfile(Request $request): Response
{ {
$user = $this->auth->user(); $user = $this->auth->user();
config('buildup_start');
config('teardown_end');
$data = $this->validate($request, [ $data = $this->validate($request, [
'pronoun' => 'optional|max:15', 'pronoun' => 'optional|max:15',
'first_name' => 'optional|max:64', 'first_name' => 'optional|max:64',
@ -98,9 +104,19 @@ class SettingsController extends BaseController
} }
if (config('enable_planned_arrival')) { 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');
return $this->redirect->to('/settings/profile');
} else if (!$this->isDepartureDateValid($data['planned_arrival_date'], $data['planned_departure_date'])) {
$this->addNotification('settings.profile.planned_departure_date.invalid', 'errors');
return $this->redirect->to('/settings/profile');
} else {
$user->personalData->planned_arrival_date = $data['planned_arrival_date']; $user->personalData->planned_arrival_date = $data['planned_arrival_date'];
$user->personalData->planned_departure_date = $data['planned_departure_date']; $user->personalData->planned_departure_date = $data['planned_departure_date'];
} }
}
if (config('enable_dect')) { if (config('enable_dect')) {
$user->contact->dect = $data['dect']; $user->contact->dect = $data['dect'];
@ -116,7 +132,10 @@ class SettingsController extends BaseController
$user->settings->email_goody = $data['email_goody']; $user->settings->email_goody = $data['email_goody'];
} }
if (isset(config('tshirt_sizes')[$data['shirt_size']])) {
$user->personalData->shirt_size = $data['shirt_size']; $user->personalData->shirt_size = $data['shirt_size'];
$user->personalData->save();
}
$user->personalData->save(); $user->personalData->save();
$user->contact->save(); $user->contact->save();

View File

@ -0,0 +1,92 @@
<?php
namespace Engelsystem\Test\Unit\Controllers;
use Carbon\Carbon;
use Engelsystem\Config\Config;
use Engelsystem\Test\Unit\Controllers\Stub\ChecksArrivalsAndDeparturesImplementation;
use Engelsystem\Test\Unit\TestCase;
class ChecksArrivalsAndDeparturesTest extends TestCase
{
public function invalidDateCombinations(): array
{
return [
[null, null, '2022-01-16', '2022-01-15'], # arrival greater than departure
['2022-01-15', '2022-01-15', '2022-01-14', '2022-01-16'], # arrival before buildup, departure after teardown
];
}
public function validDateCombinations(): array
{
return [
[null, null, '2022-01-15', '2022-01-15'], # arrival equals departure
[null, null, '2022-01-14', '2022-01-15'], # arrival smaller than departure
['2022-01-14', null, '2022-01-14', '2022-01-15'], # arrival on buildup
['2022-01-13', null, '2022-01-14', '2022-01-15'], # arrival after buildup
[null, '2022-01-15', '2022-01-14', '2022-01-15'], # departure on teardown
[null, '2022-01-16', '2022-01-14', '2022-01-15'], # departure before teardown
['2022-01-14', '2022-01-16', '2022-01-14', '2022-01-15'], # all together
];
}
/**
* @covers \Engelsystem\Controllers\ChecksArrivalsAndDepartures::isArrivalDateValid
* @dataProvider invalidDateCombinations
*/
public function testCheckInvalidDatesForArrival($buildup, $teardown, $arrival, $departure)
{
config(['buildup_start' => is_null($buildup) ? null: new Carbon($buildup)]);
config(['teardown_end' => is_null($teardown) ? null: new Carbon($teardown)]);
$check = new ChecksArrivalsAndDeparturesImplementation();
$this->assertFalse($check->checkArrival($arrival, $departure));
}
/**
* @covers \Engelsystem\Controllers\ChecksArrivalsAndDepartures::isDepartureDateValid
* @dataProvider invalidDateCombinations
*/
public function testCheckInvalidDatesForDeparture($buildup, $teardown, $arrival, $departure)
{
config(['buildup_start' => is_null($buildup) ? null: new Carbon($buildup)]);
config(['teardown_end' => is_null($teardown) ? null: new Carbon($teardown)]);
$check = new ChecksArrivalsAndDeparturesImplementation();
$this->assertFalse($check->checkDeparture($arrival, $departure));
}
/**
* @covers \Engelsystem\Controllers\ChecksArrivalsAndDepartures::isArrivalDateValid
* @dataProvider validDateCombinations
*/
public function testCheckValidDatesForArrival($buildup, $teardown, $arrival, $departure)
{
config(['buildup_start' => is_null($buildup) ? null: new Carbon($buildup)]);
config(['teardown_end' => is_null($teardown) ? null: new Carbon($teardown)]);
$check = new ChecksArrivalsAndDeparturesImplementation();
$this->assertTrue($check->checkArrival($arrival, $departure));
}
/**
* @covers \Engelsystem\Controllers\ChecksArrivalsAndDepartures::isDepartureDateValid
* @dataProvider validDateCombinations
*/
public function testCheckValidDatesForDeparture($buildup, $teardown, $arrival, $departure)
{
config(['buildup_start' => is_null($buildup) ? null: new Carbon($buildup)]);
config(['teardown_end' => is_null($teardown) ? null: new Carbon($teardown)]);
$check = new ChecksArrivalsAndDeparturesImplementation();
$this->assertTrue($check->checkDeparture($arrival, $departure));
}
public function setUp(): void
{
parent::setUp();
$this->config = new Config();
$this->app->instance('config', $this->config);
$this->app->instance(Config::class, $this->config);
}
}

View File

@ -613,7 +613,13 @@ class SettingsControllerTest extends TestCase
'en_US' => 'English', 'en_US' => 'English',
'de_DE' => 'Deutsch' 'de_DE' => 'Deutsch'
]; ];
$this->config = new Config(['min_password_length' => 6, 'themes' => $themes, 'locales' => $languages]); $tshirt_sizes = ['S' => 'Small'];
$this->config = new Config([
'min_password_length' => 6,
'themes' => $themes,
'locales' => $languages,
'tshirt_sizes' => $tshirt_sizes
]);
$this->app->instance('config', $this->config); $this->app->instance('config', $this->config);
$this->app->instance(Config::class, $this->config); $this->app->instance(Config::class, $this->config);

View File

@ -0,0 +1,20 @@
<?php
namespace Engelsystem\Test\Unit\Controllers\Stub;
use Engelsystem\Controllers\ChecksArrivalsAndDepartures;
class ChecksArrivalsAndDeparturesImplementation
{
use ChecksArrivalsAndDepartures;
public function checkArrival(string $arrival, string $departure): bool
{
return $this->isArrivalDateValid($arrival, $departure);
}
public function checkDeparture(string $arrival, string $departure): bool
{
return $this->isDepartureDateValid($arrival, $departure);
}
}