diff --git a/src/Controllers/Admin/FaqController.php b/src/Controllers/Admin/FaqController.php
new file mode 100644
index 00000000..1a04bf42
--- /dev/null
+++ b/src/Controllers/Admin/FaqController.php
@@ -0,0 +1,126 @@
+log = $log;
+ $this->faq = $faq;
+ $this->redirect = $redirector;
+ $this->response = $response;
+ }
+
+ /**
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function edit(Request $request): Response
+ {
+ $id = $request->getAttribute('id');
+ $faq = $this->faq->find($id);
+
+ return $this->showEdit($faq);
+ }
+
+ /**
+ * @param Request $request
+ *
+ * @return Response
+ */
+ public function save(Request $request): Response
+ {
+ $id = $request->getAttribute('id');
+ /** @var Faq $faq */
+ $faq = $this->faq->findOrNew($id);
+
+ $data = $this->validate($request, [
+ 'question' => 'required',
+ 'text' => 'required',
+ 'delete' => 'optional|checked',
+ 'preview' => 'optional|checked',
+ ]);
+
+ if (!is_null($data['delete'])) {
+ $faq->delete();
+
+ $this->log->info('Deleted faq "{question}"', ['question' => $faq->question]);
+
+ $this->addNotification('faq.delete.success');
+
+ return $this->redirect->to('/faq');
+ }
+
+ $faq->question = $data['question'];
+ $faq->text = $data['text'];
+
+ if (!is_null($data['preview'])) {
+ return $this->showEdit($faq);
+ }
+
+ $faq->save();
+
+ $this->log->info('Updated faq "{question}": {text}', ['question' => $faq->question, 'text' => $faq->text]);
+
+ $this->addNotification('faq.edit.success');
+
+ return $this->redirect->to('/faq#faq-' . $faq->id);
+ }
+
+ /**
+ * @param Faq|null $faq
+ *
+ * @return Response
+ */
+ protected function showEdit(?Faq $faq): Response
+ {
+ $this->cleanupModelNullValues($faq);
+
+ return $this->response->withView(
+ 'pages/faq/edit.twig',
+ ['faq' => $faq] + $this->getNotifications()
+ );
+ }
+}
diff --git a/src/Controllers/FaqController.php b/src/Controllers/FaqController.php
new file mode 100644
index 00000000..5bf6e6c5
--- /dev/null
+++ b/src/Controllers/FaqController.php
@@ -0,0 +1,56 @@
+config = $config;
+ $this->faq = $faq;
+ $this->response = $response;
+ }
+
+ /**
+ * @return Response
+ */
+ public function index(): Response
+ {
+ $text = $this->config->get('faq_text');
+
+ $faq = $this->faq->orderBy('question')->get();
+
+ return $this->response->withView(
+ 'pages/faq/overview.twig',
+ ['text' => $text, 'items' => $faq] + $this->getNotifications()
+ );
+ }
+}
diff --git a/src/Controllers/Metrics/Controller.php b/src/Controllers/Metrics/Controller.php
index 17229a75..4a58a301 100644
--- a/src/Controllers/Metrics/Controller.php
+++ b/src/Controllers/Metrics/Controller.php
@@ -172,6 +172,7 @@ class Controller extends BaseController
['labels' => ['state' => 'answered'], 'value' => $this->stats->questions(true)],
['labels' => ['state' => 'pending'], 'value' => $this->stats->questions(false)],
],
+ 'faq' => ['type' => 'gauge', $this->stats->faq()],
'messages' => ['type' => 'gauge', $this->stats->messages()],
'password_resets' => ['type' => 'gauge', $this->stats->passwordResets()],
'registration_enabled' => ['type' => 'gauge', $this->config->get('registration_enabled')],
diff --git a/src/Controllers/Metrics/Stats.php b/src/Controllers/Metrics/Stats.php
index 78dc22a5..3147d904 100644
--- a/src/Controllers/Metrics/Stats.php
+++ b/src/Controllers/Metrics/Stats.php
@@ -7,6 +7,7 @@ namespace Engelsystem\Controllers\Metrics;
use Carbon\Carbon;
use Engelsystem\Database\Database;
use Engelsystem\Models\EventConfig;
+use Engelsystem\Models\Faq;
use Engelsystem\Models\LogEntry;
use Engelsystem\Models\Message;
use Engelsystem\Models\News;
@@ -413,6 +414,14 @@ class Stats
return $query->count();
}
+ /**
+ * @return int
+ */
+ public function faq(): int
+ {
+ return Faq::query()->count();
+ }
+
/**
* @return int
*/
diff --git a/src/Models/Faq.php b/src/Models/Faq.php
new file mode 100644
index 00000000..6f57c9b8
--- /dev/null
+++ b/src/Models/Faq.php
@@ -0,0 +1,34 @@
+ 'Foo?',
+ 'text' => 'Bar!',
+ ];
+
+ /** @var TestLogger */
+ protected $log;
+
+ /** @var Response|MockObject */
+ protected $response;
+
+ /** @var Request */
+ protected $request;
+
+ /**
+ * @covers \Engelsystem\Controllers\Admin\FaqController::__construct
+ * @covers \Engelsystem\Controllers\Admin\FaqController::edit
+ * @covers \Engelsystem\Controllers\Admin\FaqController::showEdit
+ */
+ public function testEdit()
+ {
+ $this->request->attributes->set('id', 1);
+ $this->response->expects($this->once())
+ ->method('withView')
+ ->willReturnCallback(function ($view, $data) {
+ $this->assertEquals('pages/faq/edit.twig', $view);
+
+ /** @var Collection $warnings */
+ $warnings = $data['messages'];
+ $this->assertNotEmpty($data['faq']);
+ $this->assertTrue($warnings->isEmpty());
+
+ return $this->response;
+ });
+
+ /** @var FaqController $controller */
+ $controller = $this->app->make(FaqController::class);
+
+ $controller->edit($this->request);
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\Admin\FaqController::save
+ */
+ public function testSaveCreateInvalid()
+ {
+ /** @var FaqController $controller */
+ $this->expectException(ValidationException::class);
+
+ $controller = $this->app->make(FaqController::class);
+ $controller->setValidator(new Validator());
+ $controller->save($this->request);
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\Admin\FaqController::save
+ */
+ public function testSaveCreateEdit()
+ {
+ $this->request->attributes->set('id', 2);
+ $body = $this->data;
+
+ $this->request = $this->request->withParsedBody($body);
+ $this->response->expects($this->once())
+ ->method('redirectTo')
+ ->with('http://localhost/faq#faq-2')
+ ->willReturn($this->response);
+
+ /** @var FaqController $controller */
+ $controller = $this->app->make(FaqController::class);
+ $controller->setValidator(new Validator());
+
+ $controller->save($this->request);
+
+ $this->assertTrue($this->log->hasInfoThatContains('Updated'));
+
+ /** @var Session $session */
+ $session = $this->app->get('session');
+ $messages = $session->get('messages');
+ $this->assertEquals('faq.edit.success', $messages[0]);
+
+ $faq = (new Faq())->find(2);
+ $this->assertEquals('Foo?', $faq->question);
+ $this->assertEquals('Bar!', $faq->text);
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\Admin\FaqController::save
+ */
+ public function testSavePreview()
+ {
+ $this->request->attributes->set('id', 1);
+ $this->request = $this->request->withParsedBody([
+ 'question' => 'New question',
+ 'text' => 'New text',
+ 'preview' => '1',
+ ]);
+ $this->response->expects($this->once())
+ ->method('withView')
+ ->willReturnCallback(function ($view, $data) {
+ $this->assertEquals('pages/faq/edit.twig', $view);
+
+ /** @var Faq $faq */
+ $faq = $data['faq'];
+ // Contains new text
+ $this->assertEquals('New question', $faq->question);
+ $this->assertEquals('New text', $faq->text);
+
+ return $this->response;
+ });
+
+ /** @var FaqController $controller */
+ $controller = $this->app->make(FaqController::class);
+ $controller->setValidator(new Validator());
+
+ $controller->save($this->request);
+
+ // Assert no changes
+ $faq = Faq::find(1);
+ $this->assertEquals('Lorem', $faq->question);
+ $this->assertEquals('Ipsum!', $faq->text);
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\Admin\FaqController::save
+ */
+ public function testSaveDelete()
+ {
+ $this->request->attributes->set('id', 1);
+ $this->request = $this->request->withParsedBody([
+ 'question' => '.',
+ 'text' => '.',
+ 'delete' => '1',
+ ]);
+ $this->response->expects($this->once())
+ ->method('redirectTo')
+ ->with('http://localhost/faq')
+ ->willReturn($this->response);
+
+ /** @var FaqController $controller */
+ $controller = $this->app->make(FaqController::class);
+ $controller->setValidator(new Validator());
+
+ $controller->save($this->request);
+
+ $this->assertTrue($this->log->hasInfoThatContains('Deleted'));
+
+ /** @var Session $session */
+ $session = $this->app->get('session');
+ $messages = $session->get('messages');
+ $this->assertEquals('faq.delete.success', $messages[0]);
+ }
+
+ /**
+ * Setup environment
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->initDatabase();
+
+ $this->request = Request::create('http://localhost');
+ $this->app->instance('request', $this->request);
+
+ $this->response = $this->createMock(Response::class);
+ $this->app->instance(Response::class, $this->response);
+
+ $this->log = new TestLogger();
+ $this->app->instance(LoggerInterface::class, $this->log);
+
+ $this->app->instance('session', new Session(new MockArraySessionStorage()));
+
+ $this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class);
+
+ $this->app->instance('config', new Config());
+
+ (new Faq([
+ 'question' => 'Lorem',
+ 'text' => 'Ipsum!',
+ ]))->save();
+ }
+}
diff --git a/tests/Unit/Controllers/FaqControllerTest.php b/tests/Unit/Controllers/FaqControllerTest.php
new file mode 100644
index 00000000..7b4f728d
--- /dev/null
+++ b/tests/Unit/Controllers/FaqControllerTest.php
@@ -0,0 +1,46 @@
+initDatabase();
+ (new Faq(['question' => 'Xyz', 'text' => 'Abc']))->save();
+ (new Faq(['question' => 'Something\'s wrong?', 'text' => 'Nah!']))->save();
+
+ $this->app->instance('session', new Session(new MockArraySessionStorage()));
+ $config = new Config(['faq_text' => 'Some Text']);
+ /** @var Response|MockObject $response */
+ $response = $this->createMock(Response::class);
+ $response->expects($this->once())
+ ->method('withView')
+ ->willReturnCallback(function (string $view, array $data) use ($response) {
+ $this->assertEquals('pages/faq/overview.twig', $view);
+ $this->assertEquals('Some Text', $data['text']);
+ $this->assertEquals('Nah!', $data['items'][0]->text);
+
+ return $response;
+ });
+
+ $controller = new FaqController($config, new Faq(), $response);
+ $controller->index();
+ }
+}
diff --git a/tests/Unit/Controllers/Metrics/StatsTest.php b/tests/Unit/Controllers/Metrics/StatsTest.php
index d83af3b2..54e989ed 100644
--- a/tests/Unit/Controllers/Metrics/StatsTest.php
+++ b/tests/Unit/Controllers/Metrics/StatsTest.php
@@ -4,6 +4,7 @@ namespace Engelsystem\Test\Unit\Controllers\Metrics;
use Carbon\Carbon;
use Engelsystem\Controllers\Metrics\Stats;
+use Engelsystem\Models\Faq;
use Engelsystem\Models\LogEntry;
use Engelsystem\Models\Message;
use Engelsystem\Models\News;
@@ -250,6 +251,18 @@ class StatsTest extends TestCase
$this->assertEquals(3, $stats->email('humans'));
}
+ /**
+ * @covers \Engelsystem\Controllers\Metrics\Stats::faq
+ */
+ public function testFaq()
+ {
+ (new Faq(['question' => 'Foo?', 'text' => 'Bar!']))->save();
+ (new Faq(['question' => 'Lorem??', 'text' => 'Ipsum!!!']))->save();
+
+ $stats = new Stats($this->database);
+ $this->assertEquals(2, $stats->faq());
+ }
+
/**
* @covers \Engelsystem\Controllers\Metrics\Stats::messages
*/