diff --git a/config/config.default.php b/config/config.default.php
index 00b8c92d..8b279c65 100644
--- a/config/config.default.php
+++ b/config/config.default.php
@@ -126,7 +126,7 @@ return [
],
// 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'),
// Number of News shown on one site
diff --git a/config/routes.php b/config/routes.php
index c0a562ab..f5a7e45b 100644
--- a/config/routes.php
+++ b/config/routes.php
@@ -43,6 +43,12 @@ $route->post('/news/{id:\d+}', 'NewsController@comment');
// FAQ
$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
$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
$route->addGroup(
'/news',
diff --git a/db/migrations/2020_09_30_000000_create_questions_permissions.php b/db/migrations/2020_09_30_000000_create_questions_permissions.php
new file mode 100644
index 00000000..e90a87c7
--- /dev/null
+++ b/db/migrations/2020_09_30_000000_create_questions_permissions.php
@@ -0,0 +1,65 @@
+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],
+ ]);
+ }
+}
diff --git a/includes/includes.php b/includes/includes.php
index ffbc84cc..061619dd 100644
--- a/includes/includes.php
+++ b/includes/includes.php
@@ -30,7 +30,6 @@ $includeFiles = [
__DIR__ . '/../includes/view/AngelTypes_view.php',
__DIR__ . '/../includes/view/EventConfig_view.php',
__DIR__ . '/../includes/view/PublicDashboard_view.php',
- __DIR__ . '/../includes/view/Questions_view.php',
__DIR__ . '/../includes/view/Rooms_view.php',
__DIR__ . '/../includes/view/ShiftCalendarLane.php',
__DIR__ . '/../includes/view/ShiftCalendarRenderer.php',
@@ -68,14 +67,12 @@ $includeFiles = [
__DIR__ . '/../includes/pages/admin_arrive.php',
__DIR__ . '/../includes/pages/admin_free.php',
__DIR__ . '/../includes/pages/admin_groups.php',
- __DIR__ . '/../includes/pages/admin_questions.php',
__DIR__ . '/../includes/pages/admin_rooms.php',
__DIR__ . '/../includes/pages/admin_shifts.php',
__DIR__ . '/../includes/pages/admin_user.php',
__DIR__ . '/../includes/pages/guest_login.php',
__DIR__ . '/../includes/pages/user_messages.php',
__DIR__ . '/../includes/pages/user_myshifts.php',
- __DIR__ . '/../includes/pages/user_questions.php',
__DIR__ . '/../includes/pages/user_settings.php',
__DIR__ . '/../includes/pages/user_shifts.php',
diff --git a/includes/pages/admin_questions.php b/includes/pages/admin_questions.php
deleted file mode 100644
index 1cdb8b96..00000000
--- a/includes/pages/admin_questions.php
+++ /dev/null
@@ -1,166 +0,0 @@
-can('admin_questions')) {
- $unanswered_questions = Question::unanswered()->count();
- if ($unanswered_questions > 0) {
- return ''
- . __('There are unanswered questions!')
- . '';
- }
- }
- }
-
- 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(), [
- '
' . __('Unanswered questions') . '
',
- table([
- 'from' => __('From'),
- 'question' => __('Question'),
- 'created_at' => __('Asked at'),
- 'answer' => __('Answer'),
- 'actions' => ''
- ], $unanswered_questions_table),
- '' . __('Answered questions') . '
',
- 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 '';
-}
diff --git a/includes/pages/user_questions.php b/includes/pages/user_questions.php
deleted file mode 100644
index 6c3dac92..00000000
--- a/includes/pages/user_questions.php
+++ /dev/null
@@ -1,79 +0,0 @@
-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 '';
-}
diff --git a/includes/sys_menu.php b/includes/sys_menu.php
index 484abb9b..b6dec73c 100644
--- a/includes/sys_menu.php
+++ b/includes/sys_menu.php
@@ -1,5 +1,6 @@
__('News'),
- 'meetings' => __('Meetings'),
+ 'meetings' => [__('Meetings'), 'user_meetings'],
'user_shifts' => __('Shifts'),
'angeltypes' => __('Angeltypes'),
- 'user_questions' => __('Ask the Heaven'),
+ 'questions' => [__('Ask the Heaven'), 'question.add'],
];
- foreach ($pages as $menu_page => $title) {
- if (auth()->can($menu_page) || ($menu_page == 'meetings' && auth()->can('user_meetings'))) {
- $menu[] = toolbar_item_link(page_link_to($menu_page), '', $title, $menu_page == $page);
+ foreach ($pages as $menu_page => $options) {
+ if (!menu_is_allowed($menu_page, $options)) {
+ continue;
}
+
+ $title = ((array)$options)[0];
+ $menu[] = toolbar_item_link(page_link_to($menu_page), '', $title, $menu_page == $page);
}
$menu = make_room_navigation($menu);
@@ -114,7 +118,7 @@ function make_navigation()
'admin_active' => 'Active angels',
'admin_user' => 'All Angels',
'admin_free' => 'Free angels',
- 'admin_questions' => 'Answer questions',
+ 'admin/questions' => ['Answer questions', 'question.edit'],
'shifttypes' => 'Shifttypes',
'admin_shifts' => 'Create shifts',
'admin_rooms' => 'Rooms',
@@ -129,22 +133,17 @@ function make_navigation()
}
foreach ($admin_pages as $menu_page => $options) {
- $options = (array)$options;
- $permissions = $menu_page;
- $title = $options[0];
-
- if (isset($options[1])) {
- $permissions = $options[1];
+ if (!menu_is_allowed($menu_page, $options)) {
+ continue;
}
- if (auth()->can($permissions)) {
- $admin_menu[] = toolbar_item_link(
- page_link_to($menu_page),
- '',
- __($title),
- $menu_page == $page
- );
- }
+ $title = ((array)$options)[0];
+ $admin_menu[] = toolbar_item_link(
+ page_link_to($menu_page),
+ '',
+ __($title),
+ $menu_page == $page
+ );
}
if (count($admin_menu) > 0) {
@@ -154,6 +153,24 @@ function make_navigation()
return '' . join("\n", $menu) . '
';
}
+/**
+ * @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.
*
@@ -207,3 +224,24 @@ function make_language_select()
}
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 ''
+ . __('There are unanswered questions!')
+ . '';
+}
diff --git a/includes/view/Questions_view.php b/includes/view/Questions_view.php
deleted file mode 100644
index 5d96ccd6..00000000
--- a/includes/view/Questions_view.php
+++ /dev/null
@@ -1,71 +0,0 @@
- 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);
-}
diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po
index ece9a9d3..e9aef296 100644
--- a/resources/lang/de_DE/additional.po
+++ b/resources/lang/de_DE/additional.po
@@ -114,3 +114,12 @@ msgstr "FAQ Eintrag erfolgreich gelöscht."
msgid "faq.edit.success"
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."
diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po
index a9ab1342..40fcf1f0 100644
--- a/resources/lang/de_DE/default.po
+++ b/resources/lang/de_DE/default.po
@@ -2957,3 +2957,18 @@ msgstr "Frage"
msgid "faq.message"
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"
diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po
index bcf08ef7..020d0a7e 100644
--- a/resources/lang/en_US/additional.po
+++ b/resources/lang/en_US/additional.po
@@ -110,3 +110,12 @@ msgstr "FAQ entry successfully deleted."
msgid "faq.edit.success"
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."
diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po
index 4d54671e..c5b7c777 100644
--- a/resources/lang/en_US/default.po
+++ b/resources/lang/en_US/default.po
@@ -242,3 +242,18 @@ msgstr "Question"
msgid "faq.message"
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"
diff --git a/resources/views/macros/base.twig b/resources/views/macros/base.twig
index 36d20e69..58285ba5 100644
--- a/resources/views/macros/base.twig
+++ b/resources/views/macros/base.twig
@@ -18,8 +18,8 @@
{% endmacro %}
-{% macro button(label, url, type, size) %}
-
+{% macro button(label, url, type, size, title) %}
+
{{ label }}
{% endmacro %}
diff --git a/resources/views/pages/questions/edit.twig b/resources/views/pages/questions/edit.twig
new file mode 100644
index 00000000..343ac12f
--- /dev/null
+++ b/resources/views/pages/questions/edit.twig
@@ -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 %}
+
+
{{ block('title') }}
+
+ {% include 'layouts/parts/messages.twig' %}
+
+ {% if question and question.id %}
+
+
+
+ {{ m.glyphicon('time') }} {{ question.updated_at.format(__('Y-m-d H:i')) }}
+
+ {% if question.updated_at != question.created_at %}
+ {{ __('form.updated') }}
+
+ {{ m.glyphicon('time') }} {{ question.created_at.format(__('Y-m-d H:i')) }}
+ {% endif %}
+
+
+
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/resources/views/pages/questions/overview.twig b/resources/views/pages/questions/overview.twig
new file mode 100644
index 00000000..c7ab960a
--- /dev/null
+++ b/resources/views/pages/questions/overview.twig
@@ -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 %}
+
+
+
+ {% include 'layouts/parts/messages.twig' %}
+
+
+ {% block row %}
+
+ {% block questions %}
+ {% for question in questions %}
+
+
+
+
+ {{ question.text|nl2br }}
+
+
+
+
+
+ {% if question.answer %}
+
+
+
+ {{ question.answer|markdown }}
+
+
+
+
+ {% endif %}
+
+ {% endfor %}
+ {% endblock %}
+
+ {% endblock %}
+
+
+{% endblock %}
diff --git a/src/Controllers/Admin/QuestionsController.php b/src/Controllers/Admin/QuestionsController.php
new file mode 100644
index 00000000..f1ac77f7
--- /dev/null
+++ b/src/Controllers/Admin/QuestionsController.php
@@ -0,0 +1,177 @@
+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()
+ );
+ }
+}
diff --git a/src/Controllers/QuestionsController.php b/src/Controllers/QuestionsController.php
new file mode 100644
index 00000000..808ea360
--- /dev/null
+++ b/src/Controllers/QuestionsController.php
@@ -0,0 +1,145 @@
+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');
+ }
+}
diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php
index b841e353..9edba882 100644
--- a/src/Middleware/LegacyMiddleware.php
+++ b/src/Middleware/LegacyMiddleware.php
@@ -144,10 +144,6 @@ class LegacyMiddleware implements MiddlewareInterface
$title = messages_title();
$content = user_messages();
return [$title, $content];
- case 'user_questions':
- $title = questions_title();
- $content = user_questions();
- return [$title, $content];
case 'user_settings':
$title = settings_title();
$content = user_settings();
@@ -156,10 +152,6 @@ class LegacyMiddleware implements MiddlewareInterface
$title = register_title();
$content = guest_register();
return [$title, $content];
- case 'admin_questions':
- $title = admin_questions_title();
- $content = admin_questions();
- return [$title, $content];
case 'admin_user':
$title = admin_user_title();
$content = admin_user();
diff --git a/tests/Unit/Controllers/Admin/QuestionsControllerTest.php b/tests/Unit/Controllers/Admin/QuestionsControllerTest.php
new file mode 100644
index 00000000..c23ad5e5
--- /dev/null
+++ b/tests/Unit/Controllers/Admin/QuestionsControllerTest.php
@@ -0,0 +1,267 @@
+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();
+ }
+}
diff --git a/tests/Unit/Controllers/ControllerTest.php b/tests/Unit/Controllers/ControllerTest.php
index 0f107a3e..1d2f35eb 100644
--- a/tests/Unit/Controllers/ControllerTest.php
+++ b/tests/Unit/Controllers/ControllerTest.php
@@ -35,6 +35,16 @@ abstract class ControllerTest extends TestCase
/** @var 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
*/
@@ -56,6 +66,7 @@ abstract class ControllerTest extends TestCase
$this->session = new Session(new MockArraySessionStorage());
$this->app->instance('session', $this->session);
+ $this->app->instance(Session::class, $this->session);
$this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class);
diff --git a/tests/Unit/Controllers/QuestionsControllerTest.php b/tests/Unit/Controllers/QuestionsControllerTest.php
new file mode 100644
index 00000000..e31d9bed
--- /dev/null
+++ b/tests/Unit/Controllers/QuestionsControllerTest.php
@@ -0,0 +1,215 @@
+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();
+ }
+}