Added FAQ

This commit is contained in:
Igor Scheller 2020-12-06 00:16:15 +01:00 committed by msquare
parent 5cdf3889f9
commit 857ed23548
20 changed files with 773 additions and 14 deletions

View File

@ -35,12 +35,15 @@ return [
// Footer links
'footer_items' => [
// URL to the angel faq and job description
'FAQ' => env('FAQ_URL', 'https://events.ccc.de/congress/2013/wiki/Static:Volunteers'),
'FAQ' => env('FAQ_URL', '/faq'),
// Contact email address, linked on every page
'Contact' => env('CONTACT_EMAIL', 'mailto:ticket@c3heaven.de'),
],
// Text displayed on the FAQ page, rendered as markdown
'faq_text' => env('FAQ_TEXT', null),
// Link to documentation/help
'documentation_url' => 'https://engelsystem.de/doc/',

View File

@ -40,6 +40,9 @@ $route->get('/meetings', 'NewsController@meetings');
$route->get('/news/{id:\d+}', 'NewsController@show');
$route->post('/news/{id:\d+}', 'NewsController@comment');
// FAQ
$route->get('/faq', 'FaqController@index');
// API
$route->get('/api[/{resource:.+}]', 'ApiController@index');
@ -50,6 +53,15 @@ $route->get('/design', 'DesignController@index');
$route->addGroup(
'/admin',
function (RouteCollector $route) {
// FAQ
$route->addGroup(
'/faq',
function (RouteCollector $route) {
$route->get('[/{id:\d+}]', 'Admin\\FaqController@edit');
$route->post('[/{id:\d+}]', 'Admin\\FaqController@save');
}
);
// Log
$route->get('/logs', 'Admin\\LogsController@index');
$route->post('/logs', 'Admin\\LogsController@index');

View File

@ -0,0 +1,69 @@
<?php
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateFaqTableAndPermissions extends Migration
{
/**
* Run the migration
*/
public function up()
{
$this->schema->create('faq', function (Blueprint $table) {
$table->increments('id');
$table->string('question');
$table->text('text');
$table->timestamps();
});
if ($this->schema->hasTable('Privileges')) {
$db = $this->schema->getConnection();
$db->table('Privileges')->insert([
['name' => 'faq.view', 'desc' => 'View FAQ entries'],
['name' => 'faq.edit', 'desc' => 'Edit FAQ entries'],
]);
$guestGroup = -10;
$angelGroup = -20;
$shiftCoordinatorGroup = -40;
$viewId = $db->table('Privileges')->where('name', 'faq.view')->first()->id;
$editId = $db->table('Privileges')->where('name', 'faq.edit')->first()->id;
$db->table('GroupPrivileges')->insert([
['group_id' => $guestGroup, 'privilege_id' => $viewId],
['group_id' => $angelGroup, 'privilege_id' => $viewId],
['group_id' => $shiftCoordinatorGroup, 'privilege_id' => $editId],
]);
$db->table('Privileges')
->whereIn('name', ['admin_faq'])
->delete();
}
}
/**
* Reverse the migration
*/
public function down()
{
$this->schema->drop('faq');
if ($this->schema->hasTable('Privileges')) {
$db = $this->schema->getConnection();
$db->table('Privileges')
->whereIn('name', ['faq.view', 'faq.edit'])
->delete();
$db->table('Privileges')->insert([
['name' => 'admin_faq', 'desc' => 'Edit FAQs'],
]);
$bureaucratGroup = -60;
$adminFaqId = $db->table('Privileges')->where('name', 'admin_faq')->first()->id;
$db->table('GroupPrivileges')->insert([
['group_id' => $bureaucratGroup, 'privilege_id' => $adminFaqId],
]);
}
}
}

View File

@ -108,3 +108,9 @@ msgstr "OAuth-Provider nicht gefunden"
msgid "settings.profile"
msgstr "Profil"
msgid "faq.delete.success"
msgstr "FAQ Eintrag erfolgreich gelöscht."
msgid "faq.edit.success"
msgstr "FAQ Eintrag erfolgreich aktualisiert."

View File

@ -2776,6 +2776,15 @@ msgstr "Bearbeiten"
msgid "form.save"
msgstr "Speichern"
msgid "form.preview"
msgstr "Vorschau"
msgid "form.delete"
msgstr "Löschen"
msgid "form.updated"
msgstr "Aktualisiert"
msgid "schedule.import"
msgstr "Programm importieren"
@ -2879,9 +2888,6 @@ msgstr "Treffen"
msgid "news.edit.message"
msgstr "Nachricht"
msgid "news.preview"
msgstr "Vorschau"
msgid "form.search"
msgstr "Suchen"
@ -2936,3 +2942,18 @@ msgstr "Verbinden"
msgid "form.disconnect"
msgstr "Trennen"
msgid "faq.faq"
msgstr "FAQ"
msgid "faq.edit"
msgstr "FAQ Eintrag bearbeiten"
msgid "faq.add"
msgstr "FAQ Eintrag erstellen"
msgid "faq.question"
msgstr "Frage"
msgid "faq.message"
msgstr "Antwort"

View File

@ -104,3 +104,9 @@ msgstr "Unable to find OAuth provider"
msgid "settings.profile"
msgstr "Profile"
msgid "faq.delete.success"
msgstr "FAQ entry successfully deleted."
msgid "faq.edit.success"
msgstr "FAQ entry successfully updated."

View File

@ -60,6 +60,15 @@ msgstr "Save"
msgid "form.edit"
msgstr "Bearbeiten"
msgid "form.preview"
msgstr "Preview"
msgid "form.delete"
msgstr "Delete"
msgid "form.updated"
msgstr "Updated"
msgid "schedule.import"
msgstr "Import schedule"
@ -165,9 +174,6 @@ msgstr "Meeting"
msgid "news.edit.message"
msgstr "Message"
msgid "news.preview"
msgstr "Preview"
msgid "form.search"
msgstr "Search"
@ -221,3 +227,18 @@ msgstr "Connect"
msgid "form.disconnect"
msgstr "Disconnect"
msgid "faq.faq"
msgstr "FAQ"
msgid "faq.edit"
msgstr "Edit FAQ entry"
msgid "faq.add"
msgstr "Add FAQ entry"
msgid "faq.question"
msgstr "Question"
msgid "faq.message"
msgstr "Answer"

View File

@ -26,7 +26,7 @@
{% endblock %}
{% for name,url in config('footer_items') %}
<a href="{{ url }}">
<a href="{% if url starts with '/' %}{{ url(url) }}{% else %}{{ url }}{% endif %}">
{% if '@' in url %}<span class="glyphicon glyphicon-envelope"></span>{% endif %}
{{ __(name) }}
</a> ·

View File

@ -69,6 +69,7 @@
<button class="btn btn-{{ opt.btn_type|default('primary') }} btn-{{ opt.btn_size|default('m') }}"
{%- if opt.type is defined %} type="{{ opt.type }}"{% endif %}
{%- if opt.name is defined %} name="{{ opt.name }}"{% endif %}
{%- if opt.title is defined %} title="{{ opt.title }}"{% endif %}
{%- if opt.value is defined or opt.name is defined %} value="{{ opt.value|default('1') }}"{% endif -%}
>
{{ label }}

View File

@ -0,0 +1,69 @@
{% extends 'layouts/app.twig' %}
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% block title %}{{ faq and faq.id ? __('faq.edit') : __('faq.add') }}{% endblock %}
{% block content %}
<div class="container">
<h1>{{ block('title') }}</h1>
{% include 'layouts/parts/messages.twig' %}
{% if faq and faq.id %}
<div class="row">
<div class="col-md-6">
<p>
{{ m.glyphicon('time') }} {{ faq.updated_at.format(__('Y-m-d H:i')) }}
{% if faq.updated_at != faq.created_at %}
&emsp;{{ __('form.updated') }}
<br>
{{ m.glyphicon('time') }} {{ faq.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.input('question', __('faq.question'), null, {'required': true, 'value': faq ? faq.question : ''}) }}
</div>
<div class="col-md-12">
{{ f.textarea('text', __('faq.message'), {'required': true, 'rows': 10, 'value': faq ? faq.text : ''}) }}
{{ f.submit() }}
{{ f.submit(m.glyphicon('eye-close'), {'name': 'preview', 'btn_type': 'info', 'title': __('form.preview')}) }}
{% if faq and faq.id %}
{{ f.submit(m.glyphicon('trash'), {'name': 'delete', 'btn_type': 'danger', 'title': __('form.delete')}) }}
{% endif %}
</div>
</div>
{% if faq %}
<div class="row">
<div class="col-md-12">
<h2>{{ __('form.preview') }}</h2>
<div class="panel panel-default">
<div class="panel-heading">
{{ faq.question }}
</div>
<div class="panel-body">
{{ faq.text|markdown }}
</div>
</div>
</div>
</div>
{% endif %}
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends 'layouts/app.twig' %}
{% import 'macros/base.twig' as m %}
{% block title %}
{{ __('faq.faq') }}
{% endblock %}
{% block content %}
<div class="container">
<h1>
{{ block('title') }}
{%- if has_permission_to('faq.edit') -%}
{{ m.button(m.glyphicon('plus'), url('admin/faq')) }}
{%- endif %}
</h1>
{% include 'layouts/parts/messages.twig' %}
<div class="row">
{% block text %}
{% if text|default(null) %}
<div class="col-md-12">
{{ text|markdown }}
</div>
{% endif %}
{% endblock %}
{% block row %}
{% for item in items %}
<div class="col-md-12" id="faq-{{ item.id }}">
<div class="panel panel-default">
<div class="panel-heading">
{{ item.question }}
</div>
<div class="panel-body">
{{ item.text|markdown }}
</div>
<div class="panel-footer">
{{ m.glyphicon('time') }} {{ item.updated_at.format(__('Y-m-d H:i')) }}
{% if has_permission_to('faq.edit') %}
<span class="pull-right">
{{ m.button(m.glyphicon('edit'), url('admin/faq/' ~ item.id), null, 'xs') }}
</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -2,7 +2,7 @@
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% block title %}{{ news ? __('news.edit.edit') : __('news.edit.add') }}{% endblock %}
{% block title %}{{ news and news.id ? __('news.edit.edit') : __('news.edit.add') }}{% endblock %}
{% block content %}
<div class="container">
@ -10,7 +10,7 @@
{% include 'layouts/parts/messages.twig' %}
{% if news %}
{% if news and news.id %}
<div class="row">
<div class="col-md-6">
<p>
@ -51,10 +51,10 @@
{{ f.submit() }}
{{ f.submit(m.glyphicon('eye-close'), {'name': 'preview', 'btn_type': 'info'}) }}
{{ f.submit(m.glyphicon('eye-close'), {'name': 'preview', 'btn_type': 'info', 'title': __('form.preview')}) }}
{% if news %}
{{ f.submit(m.glyphicon('trash'), {'name': 'delete', 'btn_type': 'danger'}) }}
{% if news and news.id %}
{{ f.submit(m.glyphicon('trash'), {'name': 'delete', 'btn_type': 'danger', 'title': __('form.delete')}) }}
{% endif %}
</div>
</div>
@ -62,7 +62,7 @@
{% if news %}
<div class="row">
<div class="col-md-12">
<h2>{{ __('news.preview') }}</h2>
<h2>{{ __('form.preview') }}</h2>
<div class="panel {% if not news.is_meeting %}panel-default{% else %}panel-info{% endif %}">
<div class="panel-heading">

View File

@ -0,0 +1,126 @@
<?php
namespace Engelsystem\Controllers\Admin;
use Engelsystem\Controllers\BaseController;
use Engelsystem\Controllers\CleanupModel;
use Engelsystem\Controllers\HasUserNotifications;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Models\Faq;
use Psr\Log\LoggerInterface;
class FaqController extends BaseController
{
use HasUserNotifications;
use CleanupModel;
/** @var LoggerInterface */
protected $log;
/** @var Faq */
protected $faq;
/** @var Redirector */
protected $redirect;
/** @var Response */
protected $response;
/** @var array */
protected $permissions = [
'faq.view',
'faq.edit',
];
/**
* @param LoggerInterface $log
* @param Faq $faq
* @param Redirector $redirector
* @param Response $response
*/
public function __construct(
LoggerInterface $log,
Faq $faq,
Redirector $redirector,
Response $response
) {
$this->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()
);
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Engelsystem\Controllers;
use Engelsystem\Config\Config;
use Engelsystem\Http\Response;
use Engelsystem\Models\Faq;
class FaqController extends BaseController
{
use HasUserNotifications;
/** @var Config */
protected $config;
/** @var Faq */
protected $faq;
/** @var Response */
protected $response;
/** @var string[] */
protected $permissions = [
'faq.view',
];
/**
* @param Config $config
* @param Faq $faq
* @param Response $response
*/
public function __construct(
Config $config,
Faq $faq,
Response $response
) {
$this->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()
);
}
}

View File

@ -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')],

View File

@ -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
*/

34
src/Models/Faq.php Normal file
View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
/**
* @property int $id
* @property string $question
* @property string $text
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*
* @method static Builder|Faq whereId($value)
* @method static Builder|Faq whereQuestion($value)
* @method static Builder|Faq whereText($value)
*/
class Faq extends BaseModel
{
/** @var bool Enable timestamps */
public $timestamps = true;
/** @var string The models table */
public $table = 'faq';
/** @var string[] */
protected $fillable = [
'question',
'text',
];
}

View File

@ -0,0 +1,210 @@
<?php
namespace Engelsystem\Test\Unit\Controllers\Admin;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\Admin\FaqController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Http\UrlGenerator;
use Engelsystem\Http\UrlGeneratorInterface;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\Faq;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
use Illuminate\Support\Collection;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Psr\Log\Test\TestLogger;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
class FaqControllerTest extends TestCase
{
use HasDatabase;
/** @var array */
protected $data = [
'question' => '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();
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Engelsystem\Test\Unit\Controllers;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\FaqController;
use Engelsystem\Http\Response;
use Engelsystem\Models\Faq;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
class FaqControllerTest extends TestCase
{
use HasDatabase;
/**
* @covers \Engelsystem\Controllers\FaqController::__construct
* @covers \Engelsystem\Controllers\FaqController::index
*/
public function testIndex()
{
$this->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();
}
}

View File

@ -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
*/