Added shirt edit view

This commit is contained in:
Igor Scheller 2021-09-24 20:28:51 +02:00 committed by msquare
parent 5667fc2326
commit 5c90a1ef37
14 changed files with 483 additions and 5 deletions

View File

@ -96,6 +96,16 @@ $route->addGroup(
}
);
// User
$route->addGroup(
'/user/{id:\d+}',
// Shirts
function (RouteCollector $route) {
$route->get('/shirt', 'Admin\\UserShirtController@editShirt');
$route->post('/shirt', 'Admin\\UserShirtController@saveShirt');
}
);
// News
$route->addGroup(
'/news',

View File

@ -0,0 +1,63 @@
<?php
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
class AddShirtEditPermissions extends Migration
{
/**
* Run the migration
*/
public function up()
{
if (!$this->schema->hasTable('GroupPrivileges')) {
return;
}
$db = $this->schema->getConnection();
$db->table('Privileges')
->insert(['name' => 'user.edit.shirt', 'desc' => 'Edit user shirts']);
$shiftCoordinator = -40;
$shirtManager = -30;
$userEditShirt = $db->table('Privileges')
->where('name', 'user.edit.shirt')
->get(['id'])->first();
$adminArrive = $db->table('Privileges')
->where('name', 'admin_arrive')
->get(['id'])->first();
$db->table('GroupPrivileges')
->insertOrIgnore([
['group_id' => $shiftCoordinator, 'privilege_id' => $userEditShirt->id],
['group_id' => $shirtManager, 'privilege_id' => $userEditShirt->id],
['group_id' => $shirtManager, 'privilege_id' => $adminArrive->id],
]);
}
/**
* Reverse the migration
*/
public function down()
{
if (!$this->schema->hasTable('GroupPrivileges')) {
return;
}
$db = $this->schema->getConnection();
$db->table('Privileges')
->where(['name' => 'user.edit.shirt'])
->delete();
$shirtManager = -30;
$adminArrive = $db->table('Privileges')
->where('name', 'admin_arrive')
->get(['id'])->first();
$db->table('GroupPrivileges')
->where(['group_id' => $shirtManager, 'privilege_id' => $adminArrive->id])
->delete();
}
}

View File

@ -272,6 +272,8 @@ function admin_active()
);
}
$actions[] = button(url('/admin/user/' . $usr->id . '/shirt'), __('form.edit'), 'btn-primary btn-sm');
$userData['actions'] = buttons($actions);
$matched_users[] = $userData;

View File

@ -618,6 +618,10 @@ function User_view(
div('row', [
div('col-md-12', [
buttons([
$auth->can('user.edit.shirt') ? button(
url('/admin/user/' . $user_source->id . '/shirt'),
icon('person') . __('Shirt')
) : '',
$admin_user_privilege ? button(
page_link_to('admin_user', ['id' => $user_source->id]),
icon('pencil-square') . __('edit')

View File

@ -138,3 +138,6 @@ msgstr "Es gibt eine neue News: %1$s"
msgid "notification.news.new.text"
msgstr "Du kannst sie dir unter %3$s anschauen."
msgid "user.edit.success"
msgstr "Benutzer erfolgreich bearbeitet."

View File

@ -2909,7 +2909,6 @@ msgstr "Level"
msgid "log.message"
msgstr "Nachricht"
msgid "settings.settings"
msgstr "Einstellungen"
@ -2978,3 +2977,24 @@ msgstr "Frage"
msgid "question.answer"
msgstr "Antwort"
msgid "user.edit.shirt"
msgstr "Shirt bearbeiten"
msgid "form.shirt"
msgstr "Shirt"
msgid "user.shirt_size"
msgstr "Shirt größe"
msgid "user.active"
msgstr "Aktiv"
msgid "user.force_active"
msgstr "Aktiv (erzwungen)"
msgid "user.arrived"
msgstr "Angekommen"
msgid "user.got_shirt"
msgstr "Shirt bekommen"

View File

@ -134,3 +134,6 @@ msgstr "A new news is available: %1$s"
msgid "notification.news.new.text"
msgstr "You can watch it at %3$s"
msgid "user.edit.success"
msgstr "User edited successfully."

View File

@ -260,3 +260,24 @@ msgstr "Question"
msgid "question.answer"
msgstr "Answer"
msgid "user.edit.shirt"
msgstr "Edit shirt"
msgid "form.shirt"
msgstr "Shirt"
msgid "user.shirt_size"
msgstr "Shirt size"
msgid "user.active"
msgstr "Active"
msgid "user.force_active"
msgstr "Active (forced)"
msgid "user.arrived"
msgstr "Arrived"
msgid "user.got_shirt"
msgstr "Got shirt"

View File

@ -0,0 +1,39 @@
{% extends "layouts/app.twig" %}
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% block title %}{{ __('user.edit.shirt') }}{% endblock %}
{% block content %}
<div class="container">
<h1>{{ block('title') }}</h1>
{% include 'layouts/parts/messages.twig' %}
<form method="post">
{{ csrf() }}
<div class="row">
<div class="col-md-4">
{{ f.select('shirt_size', config('tshirt_sizes'), __('user.shirt_size'), userdata.personalData.shirt_size) }}
</div>
<div class="col-md-8">
{% if has_permission_to('admin_arrive') %}
{{ f.switch('arrived', __('user.arrived'), userdata.state.arrived) }}
{% endif %}
{% if userdata.state.force_active %}
{{ f.switch('force_active', __('user.force_active'), true, {'disabled': true}) }}
{% endif %}
{{ f.switch('active', __('user.active'), userdata.state.active) }}
{{ f.switch('got_shirt', __('user.got_shirt'), userdata.state.got_shirt) }}
</div>
<div class="col-md-12">
{{ f.submit(__('form.save')) }}
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -44,17 +44,18 @@
{% endif %}
<select id="{{ name }}" name="{{ name }}" class="form-control">
{% for value,decription in data -%}
<option value="{{ value }}" {% if value == selected %} selected{% endif %}>{{ decription }}</option>
<option value="{{ value }}"{% if value == selected %} selected{% endif %}>{{ decription }}</option>
{% endfor %}
</select>
</div>
{%- endmacro %}
{% macro checkbox(name, label, checked, value) %}
<div class="checkbox">
{% macro checkbox(name, label, checked, value, disabled) %}
<div class="form-check">
<label>
<input type="checkbox" id="{{ name }}" name="{{ name }}" value="{{ value|default('1') }}"
<input type="checkbox" id="{{ name }}" name="{{ name }}" value="{{ value|default('1') }}" class="form-check-input"
{%- if checked|default(false) %} checked{% endif %}
{%- if disabled|default(false) %} disabled{% endif %}
>
{{ label }}
</label>
@ -81,3 +82,13 @@
{% macro submit(label, opt) %}
{{ _self.button(label|default(__('form.submit')), {'type': 'submit', 'btn_type': 'primary'}|merge(opt|default({}))) }}
{%- endmacro %}
{% macro switch(name, label, checked, opt) %}
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="{{ name }}" name="{{ name }}" value="{{ opt.value|default('1') }}"
{%- if checked|default(false) %} checked{% endif %}
{%- if opt.disabled|default(false) %} disabled{% endif %}
>
<label class="form-check-label" for="{{ name }}">{{ label }}</label>
</div>
{%- endmacro %}

View File

@ -0,0 +1,129 @@
<?php
namespace Engelsystem\Controllers\Admin;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\BaseController;
use Engelsystem\Controllers\HasUserNotifications;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Models\User\User;
use Psr\Log\LoggerInterface;
class UserShirtController extends BaseController
{
use HasUserNotifications;
/** @var Authenticator */
protected $auth;
/** @var Config */
protected $config;
/** @var LoggerInterface */
protected $log;
/** @var Redirector */
protected $redirect;
/** @var Response */
protected $response;
/** @var User */
protected $user;
/** @var array */
protected $permissions = [
'editShirt' => 'user.edit.shirt',
'saveShirt' => 'user.edit.shirt',
];
/**
* @param Authenticator $auth
* @param Config $config
* @param LoggerInterface $log
* @param Redirector $redirector
* @param Response $response
* @param User $user
*/
public function __construct(
Authenticator $auth,
Config $config,
LoggerInterface $log,
Redirector $redirector,
Response $response,
User $user
) {
$this->auth = $auth;
$this->config = $config;
$this->log = $log;
$this->redirect = $redirector;
$this->response = $response;
$this->user = $user;
}
/**
* @param Request $request
*
* @return Response
*/
public function editShirt(Request $request): Response
{
$id = $request->getAttribute('id');
$user = $this->user->findOrFail($id);
return $this->response->withView(
'admin/user/edit-shirt.twig',
['userdata' => $user] + $this->getNotifications()
);
}
/**
* @param Request $request
*
* @return Response
*/
public function saveShirt(Request $request): Response
{
$id = $request->getAttribute('id');
/** @var User $user */
$user = $this->user->findOrFail($id);
$data = $this->validate($request, [
'shirt_size' => 'required',
'arrived' => 'optional|checked',
'active' => 'optional|checked',
'got_shirt' => 'optional|checked',
]);
if (isset($this->config->get('tshirt_sizes')[$data['shirt_size']])) {
$user->personalData->shirt_size = $data['shirt_size'];
$user->personalData->save();
}
if ($this->auth->can('admin_arrive')) {
$user->state->arrived = (bool)$data['arrived'];
}
$user->state->active = (bool)$data['active'];
$user->state->got_shirt = (bool)$data['got_shirt'];
$user->state->save();
$this->log->info(
'Updated user shirt state "{user}" ({id}): {size}, arrived: {arrived}, got shirt: {got_shirt}',
[
'id' => $user->id,
'user' => $user->name,
'size' => $user->personalData->shirt_size,
'arrived' => $user->state->arrived,
'got_shirt' => $user->state->got_shirt
]
);
$this->addNotification('user.edit.success');
return $this->redirect->back();
}
}

View File

@ -33,6 +33,15 @@ class State extends HasUserModel
'arrival_date',
];
/** @var array */
protected $casts = [
'arrived' => 'boolean',
'active' => 'boolean',
'force_active' => 'boolean',
'got_shirt' => 'boolean',
'got_voucher' => 'integer',
];
/** The attributes that are mass assignable */
protected $fillable = [
'user_id',

View File

@ -0,0 +1,163 @@
<?php
namespace Engelsystem\Test\Unit\Controllers\Admin;
use Engelsystem\Controllers\Admin\UserShirtController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\User\PersonalData;
use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\Controllers\ControllerTest;
use Engelsystem\Test\Unit\HasDatabase;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use PHPUnit\Framework\MockObject\MockObject;
class UserShirtControllerTest extends ControllerTest
{
use HasDatabase;
/**
* @covers \Engelsystem\Controllers\Admin\UserShirtController::editShirt
* @covers \Engelsystem\Controllers\Admin\UserShirtController::__construct
*/
public function testIndex()
{
$request = $this->request->withAttribute('id', 1);
/** @var Authenticator|MockObject $auth */
$auth = $this->createMock(Authenticator::class);
/** @var Redirector|MockObject $redirector */
$redirector = $this->createMock(Redirector::class);
$user = new User();
User::factory()->create();
$this->setExpects($this->response, 'withView', ['admin/user/edit-shirt.twig'], $this->response);
$controller = new UserShirtController($auth, $this->config, $this->log, $redirector, $this->response, $user);
$controller->editShirt($request);
}
/**
* @covers \Engelsystem\Controllers\Admin\UserShirtController::editShirt
*/
public function testIndexUserNotFound()
{
/** @var Authenticator|MockObject $auth */
$auth = $this->createMock(Authenticator::class);
/** @var Redirector|MockObject $redirector */
$redirector = $this->createMock(Redirector::class);
$user = new User();
$controller = new UserShirtController($auth, $this->config, $this->log, $redirector, $this->response, $user);
$this->expectException(ModelNotFoundException::class);
$controller->editShirt($this->request);
}
/**
* @covers \Engelsystem\Controllers\Admin\UserShirtController::saveShirt
*/
public function testSaveShirt()
{
$request = $this->request
->withAttribute('id', 1)
->withParsedBody([
'shirt_size' => 'S',
]);
/** @var Authenticator|MockObject $auth */
$auth = $this->createMock(Authenticator::class);
$this->config->set('tshirt_sizes', ['S' => 'Small']);
/** @var Redirector|MockObject $redirector */
$redirector = $this->createMock(Redirector::class);
User::factory()
->has(State::factory())
->has(PersonalData::factory())
->create();
$auth
->expects($this->exactly(4))
->method('can')
->with('admin_arrive')
->willReturnOnConsecutiveCalls(true, true, true, false);
$this->setExpects($redirector, 'back', null, $this->response, $this->exactly(4));
$controller = new UserShirtController(
$auth,
$this->config,
$this->log,
$redirector,
$this->response,
new User()
);
$controller->setValidator(new Validator());
// Set shirt size
$controller->saveShirt($request);
$this->assertHasNotification('user.edit.success');
$this->assertTrue($this->log->hasInfoThatContains('Updated user shirt state'));
$user = User::find(1);
$this->assertEquals('S', $user->personalData->shirt_size);
$this->assertFalse($user->state->arrived);
$this->assertFalse($user->state->active);
$this->assertFalse($user->state->got_shirt);
// Set active, arrived and got_shirt
$request = $request
->withParsedBody([
'shirt_size' => 'S',
'arrived' => '1',
'active' => '1',
'got_shirt' => '1',
]);
$controller->saveShirt($request);
$user = User::find(1);
$this->assertTrue($user->state->active);
$this->assertTrue($user->state->arrived);
$this->assertTrue($user->state->got_shirt);
// Shirt size not available
$request = $request
->withParsedBody([
'shirt_size' => 'L',
]);
$controller->saveShirt($request);
$user = User::find(1);
$this->assertEquals('S', $user->personalData->shirt_size);
// Not allowed changing arrived
$request = $request
->withParsedBody([
'shirt_size' => 'S',
'arrived' => '1',
]);
$this->assertFalse($user->state->arrived);
$controller->saveShirt($request);
$user = User::find(1);
$this->assertFalse($user->state->arrived);
}
/**
* @covers \Engelsystem\Controllers\Admin\UserShirtController::saveShirt
*/
public function testSaveShirtUserNotFound()
{
/** @var Authenticator|MockObject $auth */
$auth = $this->createMock(Authenticator::class);
/** @var Redirector|MockObject $redirector */
$redirector = $this->createMock(Redirector::class);
$user = new User();
$controller = new UserShirtController($auth, $this->config, $this->log, $redirector, $this->response, $user);
$this->expectException(ModelNotFoundException::class);
$controller->editShirt($this->request);
}
}

View File

@ -53,6 +53,7 @@ trait HasDatabase
['migration' => '2020_04_07_000000_change_mysql_database_encoding_to_utf8mb4'],
['migration' => '2020_09_12_000000_create_welcome_angel_permissions_group'],
['migration' => '2020_12_28_000000_oauth_set_identifier_binary'],
['migration' => '2021_08_26_000000_add_shirt_edit_permissions'],
]
);