Add new questions frontend

This commit is contained in:
Igor Scheller 2020-12-18 18:27:10 +01:00 committed by msquare
parent e322867716
commit b5c974b9e3
21 changed files with 1168 additions and 350 deletions

View File

@ -126,7 +126,7 @@ return [
], ],
// Redirect to this site after logging in or when pressing the top-left button // Redirect to this site after logging in or when pressing the top-left button
// Must be one of news, meetings, user_shifts, angeltypes, user_questions // Must be one of news, meetings, user_shifts, angeltypes, questions
'home_site' => env('HOME_SITE', 'news'), 'home_site' => env('HOME_SITE', 'news'),
// Number of News shown on one site // Number of News shown on one site

View File

@ -43,6 +43,12 @@ $route->post('/news/{id:\d+}', 'NewsController@comment');
// FAQ // FAQ
$route->get('/faq', 'FaqController@index'); $route->get('/faq', 'FaqController@index');
// Questions
$route->get('/questions', 'QuestionsController@index');
$route->post('/questions', 'QuestionsController@delete');
$route->get('/questions/new', 'QuestionsController@add');
$route->post('/questions/new', 'QuestionsController@save');
// API // API
$route->get('/api[/{resource:.+}]', 'ApiController@index'); $route->get('/api[/{resource:.+}]', 'ApiController@index');
@ -78,6 +84,17 @@ $route->addGroup(
} }
); );
// Questions
$route->addGroup(
'/questions',
function (RouteCollector $route) {
$route->get('', 'Admin\\QuestionsController@index');
$route->post('', 'Admin\\QuestionsController@delete');
$route->get('/{id:\d+}', 'Admin\\QuestionsController@edit');
$route->post('/{id:\d+}', 'Admin\\QuestionsController@save');
}
);
// News // News
$route->addGroup( $route->addGroup(
'/news', '/news',

View File

@ -0,0 +1,65 @@
<?php
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
class CreateQuestionsPermissions extends Migration
{
/**
* Run the migration
*/
public function up()
{
if ($this->schema->hasTable('Privileges')) {
$db = $this->schema->getConnection();
$db->table('Privileges')->insert([
['name' => 'question.add', 'desc' => 'Ask questions'],
['name' => 'question.edit', 'desc' => 'Answer questions'],
]);
$userGroup = -20;
$shiftCoordinatorGroup = -60;
$addId = $db->table('Privileges')->where('name', 'question.add')->first()->id;
$editId = $db->table('Privileges')->where('name', 'question.edit')->first()->id;
$db->table('GroupPrivileges')->insert([
['group_id' => $userGroup, 'privilege_id' => $addId],
['group_id' => $shiftCoordinatorGroup, 'privilege_id' => $editId],
]);
$db->table('Privileges')
->whereIn('name', ['user_questions', 'admin_questions'])
->delete();
}
}
/**
* Reverse the migration
*/
public function down()
{
if (!$this->schema->hasTable('Privileges')) {
return;
}
$db = $this->schema->getConnection();
$db->table('Privileges')
->whereIn('name', ['question.add', 'question.edit'])
->delete();
$db->table('Privileges')->insert([
['name' => 'user_questions', 'desc' => 'Let users ask questions'],
['name' => 'admin_questions', 'desc' => 'Answer user\'s questions'],
]);
$userGroup = -20;
$shiftCoordinatorGroup = -60;
$bureaucratGroup = -40;
$userQuestionsId = $db->table('Privileges')->where('name', 'user_questions')->first()->id;
$adminQuestionsId = $db->table('Privileges')->where('name', 'admin_questions')->first()->id;
$db->table('GroupPrivileges')->insert([
['group_id' => $userGroup, 'privilege_id' => $userQuestionsId],
['group_id' => $shiftCoordinatorGroup, 'privilege_id' => $adminQuestionsId],
['group_id' => $bureaucratGroup, 'privilege_id' => $adminQuestionsId],
]);
}
}

View File

@ -30,7 +30,6 @@ $includeFiles = [
__DIR__ . '/../includes/view/AngelTypes_view.php', __DIR__ . '/../includes/view/AngelTypes_view.php',
__DIR__ . '/../includes/view/EventConfig_view.php', __DIR__ . '/../includes/view/EventConfig_view.php',
__DIR__ . '/../includes/view/PublicDashboard_view.php', __DIR__ . '/../includes/view/PublicDashboard_view.php',
__DIR__ . '/../includes/view/Questions_view.php',
__DIR__ . '/../includes/view/Rooms_view.php', __DIR__ . '/../includes/view/Rooms_view.php',
__DIR__ . '/../includes/view/ShiftCalendarLane.php', __DIR__ . '/../includes/view/ShiftCalendarLane.php',
__DIR__ . '/../includes/view/ShiftCalendarRenderer.php', __DIR__ . '/../includes/view/ShiftCalendarRenderer.php',
@ -68,14 +67,12 @@ $includeFiles = [
__DIR__ . '/../includes/pages/admin_arrive.php', __DIR__ . '/../includes/pages/admin_arrive.php',
__DIR__ . '/../includes/pages/admin_free.php', __DIR__ . '/../includes/pages/admin_free.php',
__DIR__ . '/../includes/pages/admin_groups.php', __DIR__ . '/../includes/pages/admin_groups.php',
__DIR__ . '/../includes/pages/admin_questions.php',
__DIR__ . '/../includes/pages/admin_rooms.php', __DIR__ . '/../includes/pages/admin_rooms.php',
__DIR__ . '/../includes/pages/admin_shifts.php', __DIR__ . '/../includes/pages/admin_shifts.php',
__DIR__ . '/../includes/pages/admin_user.php', __DIR__ . '/../includes/pages/admin_user.php',
__DIR__ . '/../includes/pages/guest_login.php', __DIR__ . '/../includes/pages/guest_login.php',
__DIR__ . '/../includes/pages/user_messages.php', __DIR__ . '/../includes/pages/user_messages.php',
__DIR__ . '/../includes/pages/user_myshifts.php', __DIR__ . '/../includes/pages/user_myshifts.php',
__DIR__ . '/../includes/pages/user_questions.php',
__DIR__ . '/../includes/pages/user_settings.php', __DIR__ . '/../includes/pages/user_settings.php',
__DIR__ . '/../includes/pages/user_shifts.php', __DIR__ . '/../includes/pages/user_shifts.php',

View File

@ -1,166 +0,0 @@
<?php
use Carbon\Carbon;
use Engelsystem\Models\Question;
/**
* @return string
*/
function admin_questions_title()
{
return __('Answer questions');
}
/**
* Renders a hint for new questions to answer.
*
* @return string|null
*/
function admin_new_questions()
{
if (current_page() != 'admin_questions') {
if (auth()->can('admin_questions')) {
$unanswered_questions = Question::unanswered()->count();
if ($unanswered_questions > 0) {
return '<a href="' . page_link_to('admin_questions') . '">'
. __('There are unanswered questions!')
. '</a>';
}
}
}
return null;
}
/**
* @return string
*/
function admin_questions()
{
$user = auth()->user();
$request = request();
if (!$request->has('action')) {
$unanswered_questions_table = [];
$unanswered_questions = Question::unanswered()->orderByDesc('created_at')->get();
foreach ($unanswered_questions as $question) {
/* @var Question $question */
$user_source = $question->user;
$unanswered_questions_table[] = [
'from' => User_Nick_render($user_source) . User_Pronoun_render($user_source),
'question' => nl2br(htmlspecialchars($question->text)),
'created_at' => $question->created_at,
'answer' => form([
form_textarea('answer', '', ''),
form_submit('submit', __('Send'))
], page_link_to('admin_questions', ['action' => 'answer', 'id' => $question->id])),
'actions' => form([
form_submit('submit', __('delete'), 'btn-xs'),
], page_link_to('admin_questions', ['action' => 'delete', 'id' => $question->id])),
];
}
$answered_questions_table = [];
$answered_questions = Question::answered()->orderByDesc('answered_at')->get();
foreach ($answered_questions as $question) {
/* @var Question $question */
$user_source = $question->user;
$answer_user_source = $question->answerer;
$answered_questions_table[] = [
'from' => User_Nick_render($user_source),
'question' => nl2br(htmlspecialchars($question->text)),
'created_at' => $question->created_at,
'answered_by' => User_Nick_render($answer_user_source),
'answer' => nl2br(htmlspecialchars($question->answer)),
'answered_at' => $question->answered_at,
'actions' => form([
form_submit('submit', __('delete'), 'btn-xs')
], page_link_to('admin_questions', ['action' => 'delete', 'id' => $question->id]))
];
}
return page_with_title(admin_questions_title(), [
'<h2>' . __('Unanswered questions') . '</h2>',
table([
'from' => __('From'),
'question' => __('Question'),
'created_at' => __('Asked at'),
'answer' => __('Answer'),
'actions' => ''
], $unanswered_questions_table),
'<h2>' . __('Answered questions') . '</h2>',
table([
'from' => __('From'),
'question' => __('Question'),
'created_at' => __('Asked at'),
'answered_by' => __('Answered by'),
'answer' => __('Answer'),
'answered_at' => __('Answered at'),
'actions' => ''
], $answered_questions_table)
]);
} else {
switch ($request->input('action')) {
case 'answer':
if (
$request->has('id')
&& preg_match('/^\d{1,11}$/', $request->input('id'))
&& $request->hasPostData('submit')
) {
$question_id = $request->input('id');
} else {
return error('Incomplete call, missing Question ID.', true);
}
$question = Question::find($question_id);
if (!empty($question) && empty($question->answerer_id)) {
$answer = trim($request->input('answer'));
if (!empty($answer)) {
$question->answerer_id = $user->id;
$question->answer = $answer;
$question->answered_at = Carbon::now();
$question->save();
engelsystem_log(
'Question '
. $question->text
. ' (' . $question->id . ')'
. ' answered: '
. $answer
);
throw_redirect(page_link_to('admin_questions'));
} else {
return error('Enter an answer!', true);
}
} else {
return error('No question found.', true);
}
break;
case 'delete':
if (
$request->has('id')
&& preg_match('/^\d{1,11}$/', $request->input('id'))
&& $request->hasPostData('submit')
) {
$question_id = $request->input('id');
} else {
return error('Incomplete call, missing Question ID.', true);
}
$question = Question::find($question_id);
if (!empty($question)) {
$question->delete();
engelsystem_log('Question deleted: ' . $question->text);
throw_redirect(page_link_to('admin_questions'));
} else {
return error('No question found.', true);
}
break;
}
}
return '';
}

View File

@ -1,79 +0,0 @@
<?php
use Engelsystem\Models\Question;
/**
* @return string
*/
function questions_title()
{
return __('Ask the Heaven');
}
/**
* @return string
*/
function user_questions()
{
$user = auth()->user();
$request = request();
if (!$request->has('action')) {
$open_questions = $user->questionsAsked()
->whereNull('answerer_id')
->orderByDesc('created_at')
->get();
$answered_questions = $user->questionsAsked()
->whereNotNull('answerer_id')
->orderByDesc('answered_at')
->get();
return Questions_view(
$open_questions->all(),
$answered_questions->all(),
page_link_to('user_questions', ['action' => 'ask'])
);
} else {
switch ($request->input('action')) {
case 'ask':
$question = request()->get('question');
if (!empty($question) && $request->hasPostData('submit')) {
Question::create([
'user_id' => $user->id,
'text' => $question,
]);
success(__('You question was saved.'));
throw_redirect(page_link_to('user_questions'));
} else {
return page_with_title(questions_title(), [
error(__('Please enter a question!'), true)
]);
}
break;
case 'delete':
if (
$request->has('id')
&& preg_match('/^\d{1,11}$/', $request->input('id'))
&& $request->hasPostData('submit')
) {
$question_id = $request->input('id');
} else {
return error(__('Incomplete call, missing Question ID.'), true);
}
$question = Question::find($question_id);
if (!empty($question) && $question->user_id == $user->id) {
$question->delete();
throw_redirect(page_link_to('user_questions'));
} else {
return page_with_title(questions_title(), [
error(__('No question found.'), true)
]);
}
break;
}
}
return '';
}

View File

@ -1,5 +1,6 @@
<?php <?php
use Engelsystem\Models\Question;
use Engelsystem\UserHintsRenderer; use Engelsystem\UserHintsRenderer;
/** /**
@ -92,16 +93,19 @@ function make_navigation()
$menu = []; $menu = [];
$pages = [ $pages = [
'news' => __('News'), 'news' => __('News'),
'meetings' => __('Meetings'), 'meetings' => [__('Meetings'), 'user_meetings'],
'user_shifts' => __('Shifts'), 'user_shifts' => __('Shifts'),
'angeltypes' => __('Angeltypes'), 'angeltypes' => __('Angeltypes'),
'user_questions' => __('Ask the Heaven'), 'questions' => [__('Ask the Heaven'), 'question.add'],
]; ];
foreach ($pages as $menu_page => $title) { foreach ($pages as $menu_page => $options) {
if (auth()->can($menu_page) || ($menu_page == 'meetings' && auth()->can('user_meetings'))) { if (!menu_is_allowed($menu_page, $options)) {
$menu[] = toolbar_item_link(page_link_to($menu_page), '', $title, $menu_page == $page); continue;
} }
$title = ((array)$options)[0];
$menu[] = toolbar_item_link(page_link_to($menu_page), '', $title, $menu_page == $page);
} }
$menu = make_room_navigation($menu); $menu = make_room_navigation($menu);
@ -114,7 +118,7 @@ function make_navigation()
'admin_active' => 'Active angels', 'admin_active' => 'Active angels',
'admin_user' => 'All Angels', 'admin_user' => 'All Angels',
'admin_free' => 'Free angels', 'admin_free' => 'Free angels',
'admin_questions' => 'Answer questions', 'admin/questions' => ['Answer questions', 'question.edit'],
'shifttypes' => 'Shifttypes', 'shifttypes' => 'Shifttypes',
'admin_shifts' => 'Create shifts', 'admin_shifts' => 'Create shifts',
'admin_rooms' => 'Rooms', 'admin_rooms' => 'Rooms',
@ -129,22 +133,17 @@ function make_navigation()
} }
foreach ($admin_pages as $menu_page => $options) { foreach ($admin_pages as $menu_page => $options) {
$options = (array)$options; if (!menu_is_allowed($menu_page, $options)) {
$permissions = $menu_page; continue;
$title = $options[0];
if (isset($options[1])) {
$permissions = $options[1];
} }
if (auth()->can($permissions)) { $title = ((array)$options)[0];
$admin_menu[] = toolbar_item_link( $admin_menu[] = toolbar_item_link(
page_link_to($menu_page), page_link_to($menu_page),
'', '',
__($title), __($title),
$menu_page == $page $menu_page == $page
); );
}
} }
if (count($admin_menu) > 0) { if (count($admin_menu) > 0) {
@ -154,6 +153,24 @@ function make_navigation()
return '<ul class="nav navbar-nav">' . join("\n", $menu) . '</ul>'; return '<ul class="nav navbar-nav">' . join("\n", $menu) . '</ul>';
} }
/**
* @param string $page
* @param string|string[] $options
*
* @return bool
*/
function menu_is_allowed(string $page, $options)
{
$options = (array)$options;
$permissions = $page;
if (isset($options[1])) {
$permissions = $options[1];
}
return auth()->can($permissions);
}
/** /**
* Adds room navigation to the given menu. * Adds room navigation to the given menu.
* *
@ -207,3 +224,24 @@ function make_language_select()
} }
return $items; return $items;
} }
/**
* Renders a hint for new questions to answer.
*
* @return string|null
*/
function admin_new_questions()
{
if (!auth()->can('question.edit') || current_page() == 'admin/questions') {
return null;
}
$unanswered_questions = Question::unanswered()->count();
if (!$unanswered_questions) {
return null;
}
return '<a href="' . page_link_to('/admin/questions') . '">'
. __('There are unanswered questions!')
. '</a>';
}

View File

@ -1,71 +0,0 @@
<?php
use Engelsystem\Models\Question;
/**
* @param Question[] $open_questions
* @param Question[] $answered_questions
* @param string $ask_action
* @return string
*/
function Questions_view(array $open_questions, array $answered_questions, $ask_action)
{
$open_questions = array_map(
static function (Question $question): array {
return [
'actions' => form(
[
form_submit('submit', __('delete'), 'btn-default btn-xs')
],
page_link_to('user_questions', ['action' => 'delete', 'id' => $question->id])
),
'Question' => nl2br(htmlspecialchars($question->text)),
'created_at' => $question->created_at,
];
},
$open_questions
);
$answered_questions = array_map(
static function (Question $question): array {
return [
'Question' => nl2br(htmlspecialchars($question->text)),
'created_at' => $question->created_at,
'Answer' => nl2br(htmlspecialchars($question->answer)),
'answer_user' => User_Nick_render($question->answerer),
'answered_at' => $question->answered_at,
'actions' => form(
[
form_submit('submit', __('delete'), 'btn-default btn-xs')
],
page_link_to('user_questions', ['action' => 'delete', 'id' => $question->id])
),
];
},
$answered_questions
);
return page_with_title(questions_title(), [
msg(),
heading(__('Open questions'), 2),
table([
'Question' => __('Question'),
'created_at' => __('Asked at'),
'actions' => ''
], $open_questions),
heading(__('Answered questions'), 2),
table([
'Question' => __('Question'),
'created_at' => __('Asked at'),
'answer_user' => __('Answered by'),
'Answer' => __('Answer'),
'answered_at' => __('Answered at'),
'actions' => ''
], $answered_questions),
heading(__('Ask the Heaven'), 2),
form([
form_textarea('question', __('Your Question:'), ''),
form_submit('submit', __('Send'))
], $ask_action)
], true);
}

View File

@ -114,3 +114,12 @@ msgstr "FAQ Eintrag erfolgreich gelöscht."
msgid "faq.edit.success" msgid "faq.edit.success"
msgstr "FAQ Eintrag erfolgreich aktualisiert." msgstr "FAQ Eintrag erfolgreich aktualisiert."
msgid "question.delete.success"
msgstr "Frage erfolgreich gelöscht."
msgid "question.add.success"
msgstr "Frage erstellt."
msgid "question.edit.success"
msgstr "Frage erfolgreich bearbeitet."

View File

@ -2957,3 +2957,18 @@ msgstr "Frage"
msgid "faq.message" msgid "faq.message"
msgstr "Antwort" msgstr "Antwort"
msgid "question.questions"
msgstr "Fragen"
msgid "question.add"
msgstr "Fragen"
msgid "question.edit"
msgstr "Frage bearbeiten"
msgid "question.question"
msgstr "Frage"
msgid "question.answer"
msgstr "Antwort"

View File

@ -110,3 +110,12 @@ msgstr "FAQ entry successfully deleted."
msgid "faq.edit.success" msgid "faq.edit.success"
msgstr "FAQ entry successfully updated." msgstr "FAQ entry successfully updated."
msgid "question.delete.success"
msgstr "Question deleted successfully."
msgid "question.add.success"
msgstr "Question added successfully."
msgid "question.edit.success"
msgstr "Question updated successfully."

View File

@ -242,3 +242,18 @@ msgstr "Question"
msgid "faq.message" msgid "faq.message"
msgstr "Answer" msgstr "Answer"
msgid "question.questions"
msgstr "Fragen"
msgid "question.add"
msgstr "Ask"
msgid "question.edit"
msgstr "Edit question"
msgid "question.question"
msgstr "Question"
msgid "question.answer"
msgstr "Answer"

View File

@ -18,8 +18,8 @@
</a> </a>
{% endmacro %} {% endmacro %}
{% macro button(label, url, type, size) %} {% macro button(label, url, type, size, title) %}
<a href="{{ url }}" class="btn btn-{{ type|default('default') }}{% if size %} btn-{{ size }}{% endif %}"> <a href="{{ url }}" class="btn btn-{{ type|default('default') }}{% if size %} btn-{{ size }}{% endif %}"{% if title %} title="{{ title }}"{% endif %}>
{{ label }} {{ label }}
</a> </a>
{% endmacro %} {% endmacro %}

View File

@ -0,0 +1,79 @@
{% extends 'layouts/app.twig' %}
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% block title %}{{ question and question.id ? __('question.edit') : __('question.add') }}{% endblock %}
{% block content %}
<div class="container">
<h1>{{ block('title') }}</h1>
{% include 'layouts/parts/messages.twig' %}
{% if question and question.id %}
<div class="row">
<div class="col-md-6">
<p>
{{ m.glyphicon('time') }} {{ question.updated_at.format(__('Y-m-d H:i')) }}
{% if question.updated_at != question.created_at %}
&emsp;{{ __('form.updated') }}
<br>
{{ m.glyphicon('time') }} {{ question.created_at.format(__('Y-m-d H:i')) }}
{% endif %}
</p>
</div>
</div>
{% endif %}
<form action="" enctype="multipart/form-data" method="post">
{{ csrf() }}
<div class="row">
<div class="col-md-12">
{{ f.textarea('text', __('question.question'), {'required': true, 'rows': 10, 'value': question ? question.text : ''}) }}
</div>
<div class="col-md-12">
{% if is_admin|default(false) %}
{{ f.textarea('answer', __('question.answer'), {'required': true, 'rows': 10, 'value': question ? question.answer : ''}) }}
{% endif %}
{{ f.submit() }}
{% if is_admin|default(false) %}
{{ f.submit(m.glyphicon('eye-close'), {'name': 'preview', 'btn_type': 'info', 'title': __('form.preview')}) }}
{% if question and question.id %}
{{ f.submit(m.glyphicon('trash'), {'name': 'delete', 'btn_type': 'danger', 'title': __('form.delete')}) }}
{% endif %}
{% endif %}
</div>
</div>
{% if question %}
<div class="row">
<div class="col-md-12">
<h2>{{ __('form.preview') }}</h2>
<div class="panel panel-default">
<div class="panel-body">
{{ question.text|nl2br }}
</div>
</div>
</div>
{% if question.answer %}
<div class="col-md-11 col-md-offset-1">
<div class="panel panel-info">
<div class="panel-body">
{{ question.answer|markdown }}
</div>
</div>
</div>
{% endif %}
</div>
{% endif %}
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,83 @@
{% extends 'layouts/app.twig' %}
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% block title %}
{{ __('question.questions') }}
{% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>
{{ block('title') }}
{% if not is_admin|default(false) %}
{{ m.button(m.glyphicon('plus'), url('questions/new')) }}
{% endif %}
</h1>
</div>
{% include 'layouts/parts/messages.twig' %}
<div class="row">
{% block row %}
<div class="col-md-12">
{% block questions %}
{% for question in questions %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-body">
{{ question.text|nl2br }}
</div>
<div class="panel-footer">
{{ m.glyphicon('time') }} {{ question.created_at.format(__('Y-m-d H:i')) }}
{% if has_permission_to('question.edit') %}
&nbsp;{{ m.user(question.user) }}
{% endif %}
{% if question.user.id == user.id or has_permission_to('question.edit') %}
<form
action=""
enctype="multipart/form-data"
method="post"
class="pull-right"
>
{{ csrf() }}
{{ f.hidden('id', question.id) }}
{{ f.submit(m.glyphicon('trash'), {'name': 'delete', 'btn_type': 'danger', 'btn_size': 'xs', 'title': __('form.delete')}) }}
</form>
{% endif %}
{% if has_permission_to('question.edit') %}
<span class="pull-right">
{{ m.button(m.glyphicon('edit'), url('admin/questions/' ~ question.id), null, 'xs') }}
</span>
{% endif %}
</div>
</div>
</div>
{% if question.answer %}
<div class="col-md-11 col-md-offset-1">
<div class="panel panel-info">
<div class="panel-body">
{{ question.answer|markdown }}
</div>
<div class="panel-footer">
{{ m.glyphicon('time') }} {{ question.updated_at.format(__('Y-m-d H:i')) }}
&nbsp;{{ m.user(question.answerer) }}
</div>
</div>
</div>
{% endif %}
</div>
{% endfor %}
{% endblock %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,177 @@
<?php
namespace Engelsystem\Controllers\Admin;
use Carbon\Carbon;
use Engelsystem\Controllers\BaseController;
use Engelsystem\Controllers\CleanupModel;
use Engelsystem\Controllers\HasUserNotifications;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Models\Question;
use Psr\Log\LoggerInterface;
class QuestionsController extends BaseController
{
use HasUserNotifications;
use CleanupModel;
/** @var Authenticator */
protected $auth;
/** @var LoggerInterface */
protected $log;
/** @var Question */
protected $question;
/** @var Redirector */
protected $redirect;
/** @var Response */
protected $response;
/** @var array */
protected $permissions = [
'question.add',
'question.edit',
];
/**
* @param Authenticator $auth
* @param LoggerInterface $log
* @param Question $question
* @param Redirector $redirector
* @param Response $response
*/
public function __construct(
Authenticator $auth,
LoggerInterface $log,
Question $question,
Redirector $redirector,
Response $response
) {
$this->auth = $auth;
$this->log = $log;
$this->question = $question;
$this->redirect = $redirector;
$this->response = $response;
}
/**
* @return Response
*/
public function index(): Response
{
$questions = $this->question
->orderBy('answer')
->orderByDesc('created_at')
->get();
$this->cleanupModelNullValues($questions);
return $this->response->withView(
'pages/questions/overview.twig',
['questions' => $questions, 'is_admin' => true] + $this->getNotifications()
);
}
/**
* @param Request $request
*
* @return Response
*/
public function delete(Request $request): Response
{
$data = $this->validate($request, [
'id' => 'required|int',
'delete' => 'checked',
]);
$question = $this->question->findOrFail($data['id']);
$question->delete();
$this->log->info('Deleted question {question}', ['question' => $question->text]);
$this->addNotification('question.delete.success');
return $this->redirect->to('/admin/questions');
}
/**
* @param Request $request
*
* @return Response
*/
public function edit(Request $request): Response
{
$id = $request->getAttribute('id');
$questions = $this->question->find($id);
return $this->showEdit($questions);
}
/**
* @param Request $request
*
* @return Response
*/
public function save(Request $request): Response
{
$id = $request->getAttribute('id');
/** @var Question $question */
$question = $this->question->findOrNew($id);
$data = $this->validate($request, [
'text' => 'required',
'answer' => 'required',
'delete' => 'optional|checked',
'preview' => 'optional|checked',
]);
if (!is_null($data['delete'])) {
$question->delete();
$this->log->info('Deleted question "{question}"', ['question' => $question->text]);
$this->addNotification('question.delete.success');
return $this->redirect->to('/admin/questions');
}
$question->text = $data['text'];
$question->answer = $data['answer'];
$question->answered_at = Carbon::now();
$question->answerer()->associate($this->auth->user());
if (!is_null($data['preview'])) {
return $this->showEdit($question);
}
$question->save();
$this->log->info(
'Updated questions "{text}": {answer}',
['text' => $question->text, 'answer' => $question->answer]
);
$this->addNotification('question.edit.success');
return $this->redirect->to('/admin/questions');
}
/**
* @param Question|null $question
*
* @return Response
*/
protected function showEdit(?Question $question): Response
{
$this->cleanupModelNullValues($question);
return $this->response->withView(
'pages/questions/edit.twig',
['question' => $question, 'is_admin' => true] + $this->getNotifications()
);
}
}

View File

@ -0,0 +1,145 @@
<?php
namespace Engelsystem\Controllers;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Models\Question;
use Psr\Log\LoggerInterface;
class QuestionsController extends BaseController
{
use HasUserNotifications;
use CleanupModel;
/** @var Authenticator */
protected $auth;
/** @var LoggerInterface */
protected $log;
/** @var Question */
protected $question;
/** @var Redirector */
protected $redirect;
/** @var Response */
protected $response;
/** @var string[] */
protected $permissions = [
'question.add',
];
/**
* @param Authenticator $auth
* @param LoggerInterface $log
* @param Question $question
* @param Redirector $redirect
* @param Response $response
*/
public function __construct(
Authenticator $auth,
LoggerInterface $log,
Question $question,
Redirector $redirect,
Response $response
) {
$this->auth = $auth;
$this->log = $log;
$this->question = $question;
$this->redirect = $redirect;
$this->response = $response;
}
/**
* @return Response
*/
public function index(): Response
{
$questions = $this->question
->whereUserId($this->auth->user()->id)
->orderByDesc('created_at')
->get();
$this->cleanupModelNullValues($questions);
return $this->response->withView(
'pages/questions/overview.twig',
['questions' => $questions] + $this->getNotifications()
);
}
/**
* @return Response
*/
public function add(): Response
{
return $this->response->withView(
'pages/questions/edit.twig',
['question' => null] + $this->getNotifications()
);
}
/**
* @param Request $request
*
* @return Response
*/
public function delete(Request $request): Response
{
$data = $this->validate(
$request,
[
'id' => 'int|required',
'delete' => 'checked',
]
);
$question = $this->question->findOrFail($data['id']);
if ($question->user->id != $this->auth->user()->id) {
throw new HttpForbidden();
}
$question->delete();
$this->log->info('Deleted own question {question}', ['question' => $question->text]);
$this->addNotification('question.delete.success');
return $this->redirect->to('/questions');
}
/**
* @param Request $request
*
* @return Response
*/
public function save(Request $request): Response
{
$data = $this->validate(
$request,
[
'text' => 'required',
]
);
$question = new Question();
$question->user()->associate($this->auth->user());
$question->text = $data['text'];
$question->save();
$this->log->info(
'Asked: {question}',
[
'question' => $question->text,
]
);
$this->addNotification('question.add.success');
return $this->redirect->to('/questions');
}
}

View File

@ -144,10 +144,6 @@ class LegacyMiddleware implements MiddlewareInterface
$title = messages_title(); $title = messages_title();
$content = user_messages(); $content = user_messages();
return [$title, $content]; return [$title, $content];
case 'user_questions':
$title = questions_title();
$content = user_questions();
return [$title, $content];
case 'user_settings': case 'user_settings':
$title = settings_title(); $title = settings_title();
$content = user_settings(); $content = user_settings();
@ -156,10 +152,6 @@ class LegacyMiddleware implements MiddlewareInterface
$title = register_title(); $title = register_title();
$content = guest_register(); $content = guest_register();
return [$title, $content]; return [$title, $content];
case 'admin_questions':
$title = admin_questions_title();
$content = admin_questions();
return [$title, $content];
case 'admin_user': case 'admin_user':
$title = admin_user_title(); $title = admin_user_title();
$content = admin_user(); $content = admin_user();

View File

@ -0,0 +1,267 @@
<?php
namespace Engelsystem\Test\Unit\Controllers\Admin;
use Carbon\Carbon;
use Engelsystem\Controllers\Admin\QuestionsController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\UrlGenerator;
use Engelsystem\Http\UrlGeneratorInterface;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\Question;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\Controllers\ControllerTest;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use PHPUnit\Framework\MockObject\MockObject;
class QuestionsControllerTest extends ControllerTest
{
/** @var Authenticator|MockObject */
protected $auth;
/** @var User */
protected $user;
/**
* @covers \Engelsystem\Controllers\Admin\QuestionsController::index
* @covers \Engelsystem\Controllers\Admin\QuestionsController::__construct
*/
public function testIndex()
{
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$this->assertEquals('pages/questions/overview.twig', $view);
$this->assertArrayHasKey('questions', $data);
$this->assertArrayHasKey('is_admin', $data);
$this->assertEquals('Foobar?', $data['questions'][0]->text);
$this->assertTrue($data['is_admin']);
return $this->response;
});
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->index();
}
/**
* @covers \Engelsystem\Controllers\Admin\QuestionsController::delete
*/
public function testDeleteInvalidRequest()
{
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->setValidator($this->app->get(Validator::class));
$this->expectException(ValidationException::class);
$controller->delete($this->request);
}
/**
* @covers \Engelsystem\Controllers\Admin\QuestionsController::delete
*/
public function testDeleteNotFound()
{
$this->request = $this->request->withParsedBody(['id' => 42, 'delete' => '1']);
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->setValidator($this->app->get(Validator::class));
$this->expectException(ModelNotFoundException::class);
$controller->delete($this->request);
}
/**
* @covers \Engelsystem\Controllers\Admin\QuestionsController::delete
*/
public function testDelete()
{
$this->request = $this->request->withParsedBody(['id' => 1, 'delete' => '1']);
$this->setExpects($this->response, 'redirectTo', ['http://localhost/admin/questions'], $this->response);
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->setValidator($this->app->get(Validator::class));
$controller->delete($this->request);
$this->assertCount(1, Question::all());
$this->assertTrue($this->log->hasInfoThatContains('Deleted question'));
$this->assertHasNotification('question.delete.success');
}
/**
* @covers \Engelsystem\Controllers\Admin\QuestionsController::edit
* @covers \Engelsystem\Controllers\Admin\QuestionsController::showEdit
*/
public function testEdit()
{
$this->request->attributes->set('id', 1);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$this->assertEquals('pages/questions/edit.twig', $view);
$this->assertArrayHasKey('question', $data);
$this->assertArrayHasKey('is_admin', $data);
$this->assertEquals('Question?', $data['question']->text);
$this->assertEquals($this->user->id, $data['question']->user->id);
$this->assertTrue($data['is_admin']);
return $this->response;
});
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->edit($this->request);
}
/**
* @covers \Engelsystem\Controllers\Admin\QuestionsController::save
*/
public function testSaveCreateInvalid()
{
$this->expectException(ValidationException::class);
/** @var QuestionsController $controller */
$controller = $this->app->make(QuestionsController::class);
$controller->setValidator(new Validator());
$controller->save($this->request);
}
/**
* @covers \Engelsystem\Controllers\Admin\QuestionsController::save
*/
public function testSaveCreateEdit()
{
$this->request->attributes->set('id', 2);
$body = [
'text' => 'Foo?',
'answer' => 'Bar!',
];
$this->request = $this->request->withParsedBody($body);
$this->response->expects($this->once())
->method('redirectTo')
->with('http://localhost/admin/questions')
->willReturn($this->response);
/** @var QuestionsController $controller */
$controller = $this->app->make(QuestionsController::class);
$controller->setValidator(new Validator());
$controller->save($this->request);
$this->assertTrue($this->log->hasInfoThatContains('Updated'));
$this->assertHasNotification('question.edit.success');
$question = Question::find(2);
$this->assertEquals('Foo?', $question->text);
$this->assertEquals('Bar!', $question->answer);
}
/**
* @covers \Engelsystem\Controllers\Admin\QuestionsController::save
*/
public function testSavePreview()
{
$this->request->attributes->set('id', 1);
$this->request = $this->request->withParsedBody([
'text' => 'Foo?',
'answer' => 'Bar!',
'preview' => '1',
]);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function ($view, $data) {
$this->assertEquals('pages/questions/edit.twig', $view);
/** @var Question $question */
$question = $data['question'];
// Contains new text
$this->assertEquals('Foo?', $question->text);
$this->assertEquals('Bar!', $question->answer);
return $this->response;
});
/** @var QuestionsController $controller */
$controller = $this->app->make(QuestionsController::class);
$controller->setValidator(new Validator());
$controller->save($this->request);
// Assert no changes
$question = Question::find(1);
$this->assertEquals('Question?', $question->text);
$this->assertEquals('Answer!', $question->answer);
}
/**
* @covers \Engelsystem\Controllers\Admin\QuestionsController::save
*/
public function testSaveDelete()
{
$this->request->attributes->set('id', 1);
$this->request = $this->request->withParsedBody([
'text' => '.',
'answer' => '.',
'delete' => '1',
]);
$this->response->expects($this->once())
->method('redirectTo')
->with('http://localhost/admin/questions')
->willReturn($this->response);
/** @var QuestionsController $controller */
$controller = $this->app->make(QuestionsController::class);
$controller->setValidator(new Validator());
$controller->save($this->request);
$this->assertCount(1, Question::all());
$this->assertTrue($this->log->hasInfoThatContains('Deleted question'));
$this->assertHasNotification('question.delete.success');
}
/**
* Setup environment
*/
public function setUp(): void
{
parent::setUp();
$this->auth = $this->createMock(Authenticator::class);
$this->app->instance(Authenticator::class, $this->auth);
$this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class);
$this->user = new User([
'name' => 'foo',
'password' => '',
'email' => '',
'api_key' => '',
'last_login_at' => null,
]);
$this->user->save();
$this->setExpects($this->auth, 'user', null, $this->user, $this->any());
(new Question([
'user_id' => $this->user->id,
'text' => 'Question?',
'answerer_id' => $this->user->id,
'answer' => 'Answer!',
'answered_at' => new Carbon(),
]))->save();
(new Question([
'user_id' => $this->user->id,
'text' => 'Foobar?',
]))->save();
}
}

View File

@ -35,6 +35,16 @@ abstract class ControllerTest extends TestCase
/** @var Session */ /** @var Session */
protected $session; protected $session;
/**
* @param string $value
* @param string|null $message
*/
protected function assertHasNotification(string $value, string $message = null)
{
$messages = $this->session->get('messages', []);
$this->assertTrue(in_array($value, $messages), $message ?: 'Session does not contain message "' . $value . '"');
}
/** /**
* Setup environment * Setup environment
*/ */
@ -56,6 +66,7 @@ abstract class ControllerTest extends TestCase
$this->session = new Session(new MockArraySessionStorage()); $this->session = new Session(new MockArraySessionStorage());
$this->app->instance('session', $this->session); $this->app->instance('session', $this->session);
$this->app->instance(Session::class, $this->session);
$this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class); $this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class);

View File

@ -0,0 +1,215 @@
<?php
namespace Engelsystem\Test\Unit\Controllers;
use Carbon\Carbon;
use Engelsystem\Controllers\QuestionsController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\UrlGenerator;
use Engelsystem\Http\UrlGeneratorInterface;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\Question;
use Engelsystem\Models\User\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use PHPUnit\Framework\MockObject\MockObject;
class QuestionsControllerTest extends ControllerTest
{
/** @var Authenticator|MockObject */
protected $auth;
/** @var User */
protected $user;
/**
* @covers \Engelsystem\Controllers\QuestionsController::index
* @covers \Engelsystem\Controllers\QuestionsController::__construct
*/
public function testIndex()
{
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$this->assertEquals('pages/questions/overview.twig', $view);
$this->assertArrayHasKey('questions', $data);
$this->assertEquals('Lorem?', $data['questions'][0]->text);
return $this->response;
});
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->index();
}
/**
* @covers \Engelsystem\Controllers\QuestionsController::add
*/
public function testAdd()
{
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$this->assertEquals('pages/questions/edit.twig', $view);
$this->assertArrayHasKey('question', $data);
$this->assertNull($data['question']);
return $this->response;
});
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->setValidator(new Validator());
$controller->add();
}
/**
* @covers \Engelsystem\Controllers\QuestionsController::delete
*/
public function testDeleteNotFound()
{
$this->request = $this->request->withParsedBody([
'id' => '3',
'delete' => '1',
]);
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->setValidator(new Validator());
$this->expectException(ModelNotFoundException::class);
$controller->delete($this->request);
}
/**
* @covers \Engelsystem\Controllers\QuestionsController::delete
*/
public function testDeleteNotOwn()
{
$otherUser = new User([
'name' => 'bar',
'password' => '',
'email' => '.',
'api_key' => '',
'last_login_at' => null,
]);
$otherUser->save();
(new Question([
'user_id' => $otherUser->id,
'text' => 'Lorem?',
]))->save();
$this->request = $this->request->withParsedBody([
'id' => '3',
'delete' => '1',
]);
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->setValidator(new Validator());
$this->expectException(HttpForbidden::class);
$controller->delete($this->request);
}
/**
* @covers \Engelsystem\Controllers\QuestionsController::delete
*/
public function testDelete()
{
$this->request = $this->request->withParsedBody([
'id' => '2',
'delete' => '1',
]);
$this->response->expects($this->once())
->method('redirectTo')
->with('http://localhost/questions')
->willReturn($this->response);
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->setValidator(new Validator());
$controller->delete($this->request);
$this->assertCount(1, Question::all());
$this->assertTrue($this->log->hasInfoThatContains('Deleted own question'));
$this->assertHasNotification('question.delete.success');
}
/**
* @covers \Engelsystem\Controllers\QuestionsController::save
*/
public function testSaveInvalid()
{
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->setValidator(new Validator());
$this->expectException(ValidationException::class);
$controller->save($this->request);
}
/**
* @covers \Engelsystem\Controllers\QuestionsController::save
*/
public function testSave()
{
$this->request = $this->request->withParsedBody([
'text' => 'Some question?',
]);
$this->response->expects($this->once())
->method('redirectTo')
->with('http://localhost/questions')
->willReturn($this->response);
/** @var QuestionsController $controller */
$controller = $this->app->get(QuestionsController::class);
$controller->setValidator(new Validator());
$controller->save($this->request);
$this->assertCount(3, Question::all());
$this->assertTrue($this->log->hasInfoThatContains('Asked'));
$this->assertHasNotification('question.add.success');
}
/**
* Setup environment
*/
public function setUp(): void
{
parent::setUp();
$this->auth = $this->createMock(Authenticator::class);
$this->app->instance(Authenticator::class, $this->auth);
$this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class);
$this->user = new User([
'name' => 'foo',
'password' => '',
'email' => '',
'api_key' => '',
'last_login_at' => null,
]);
$this->user->save();
$this->setExpects($this->auth, 'user', null, $this->user, $this->any());
(new Question([
'user_id' => $this->user->id,
'text' => 'Lorem?',
]))->save();
(new Question([
'user_id' => $this->user->id,
'text' => 'Foo?',
'answerer_id' => $this->user->id,
'answer' => 'Bar!',
'answered_at' => new Carbon(),
]))->save();
}
}