Added email notification on new news

This commit is contained in:
Igor Scheller 2020-12-28 16:04:05 +01:00 committed by msquare
parent 814cafd05d
commit 149155fbda
22 changed files with 327 additions and 5 deletions

View File

@ -65,5 +65,6 @@ return [
// callable like [$instance, 'method] or 'function' // callable like [$instance, 'method] or 'function'
// or $function // or $function
// ] // ]
'news.created' => \Engelsystem\Events\Listener\News::class . '@created',
], ],
]; ];

View File

@ -0,0 +1,37 @@
<?php
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddEmailNewsToUsersSettings extends Migration
{
use Reference;
/**
* Run the migration
*/
public function up()
{
$this->schema->table(
'users_settings',
function (Blueprint $table) {
$table->boolean('email_news')->default(false)->after('email_shiftinfo');
}
);
}
/**
* Reverse the migration
*/
public function down()
{
$this->schema->table(
'users_settings',
function (Blueprint $table) {
$table->dropColumn('email_news');
}
);
}
}

View File

@ -46,6 +46,7 @@ function guest_register()
$pronoun = ''; $pronoun = '';
$email_shiftinfo = false; $email_shiftinfo = false;
$email_by_human_allowed = false; $email_by_human_allowed = false;
$email_news = false;
$tshirt_size = ''; $tshirt_size = '';
$password_hash = ''; $password_hash = '';
$selected_angel_types = []; $selected_angel_types = [];
@ -113,6 +114,10 @@ function guest_register()
$email_by_human_allowed = true; $email_by_human_allowed = true;
} }
if ($request->has('email_news')) {
$email_news = true;
}
if ($enable_tshirt_size) { if ($enable_tshirt_size) {
if ($request->has('tshirt_size') && isset($tshirt_sizes[$request->input('tshirt_size')])) { if ($request->has('tshirt_size') && isset($tshirt_sizes[$request->input('tshirt_size')])) {
$tshirt_size = $request->input('tshirt_size'); $tshirt_size = $request->input('tshirt_size');
@ -211,6 +216,7 @@ function guest_register()
'theme' => config('theme'), 'theme' => config('theme'),
'email_human' => $email_by_human_allowed, 'email_human' => $email_by_human_allowed,
'email_shiftinfo' => $email_shiftinfo, 'email_shiftinfo' => $email_shiftinfo,
'email_news' => $email_news,
]); ]);
$settings->user() $settings->user()
->associate($user) ->associate($user)
@ -352,11 +358,16 @@ function guest_register()
), ),
$email_shiftinfo $email_shiftinfo
), ),
form_checkbox(
'email_news',
__('Notify me of new news'),
$email_news
),
form_checkbox( form_checkbox(
'email_by_human_allowed', 'email_by_human_allowed',
__('Humans are allowed to send me an email (e.g. for ticket vouchers)'), __('Humans are allowed to send me an email (e.g. for ticket vouchers)'),
$email_by_human_allowed $email_by_human_allowed
) ),
]) ])
]), ]),
div('row', [ div('row', [

View File

@ -38,6 +38,7 @@ function user_settings_main($user_source, $enable_tshirt_size, $tshirt_sizes)
$user_source->settings->email_shiftinfo = $request->has('email_shiftinfo'); $user_source->settings->email_shiftinfo = $request->has('email_shiftinfo');
$user_source->settings->email_human = $request->has('email_by_human_allowed'); $user_source->settings->email_human = $request->has('email_by_human_allowed');
$user_source->settings->email_news = $request->has('email_news');
if ($request->has('tshirt_size') && isset($tshirt_sizes[$request->input('tshirt_size')])) { if ($request->has('tshirt_size') && isset($tshirt_sizes[$request->input('tshirt_size')])) {
$user_source->personalData->shirt_size = $request->input('tshirt_size'); $user_source->personalData->shirt_size = $request->input('tshirt_size');

View File

@ -93,6 +93,11 @@ function User_settings_view(
), ),
$user_source->settings->email_shiftinfo $user_source->settings->email_shiftinfo
), ),
form_checkbox(
'email_news',
__('Notify me of new news'),
$user_source->settings->email_news
),
form_checkbox( form_checkbox(
'email_by_human_allowed', 'email_by_human_allowed',
__('Humans are allowed to send me an email (e.g. for ticket vouchers)'), __('Humans are allowed to send me an email (e.g. for ticket vouchers)'),

View File

@ -123,3 +123,12 @@ msgstr "Frage erstellt."
msgid "question.edit.success" msgid "question.edit.success"
msgstr "Frage erfolgreich bearbeitet." msgstr "Frage erfolgreich bearbeitet."
msgid "notification.news.new"
msgstr "Neue News: %s"
msgid "notification.news.new.introduction"
msgstr "Es gibt eine neue News: %1$s"
msgid "notification.news.new.text"
msgstr "Du kannst sie dir unter %3$s anschauen."

View File

@ -1613,6 +1613,9 @@ msgstr ""
msgid "The %s is allowed to send me an email (e.g. when my shifts change)" msgid "The %s is allowed to send me an email (e.g. when my shifts change)"
msgstr "Das %s darf mir E-Mails senden (z.B. wenn sich meine Schichten ändern)" msgstr "Das %s darf mir E-Mails senden (z.B. wenn sich meine Schichten ändern)"
msgid "Notify me of new news"
msgstr "Benachrichtige mich bei neuen News"
#: includes/pages/guest_login.php:291 includes/view/User_view.php:73 #: includes/pages/guest_login.php:291 includes/view/User_view.php:73
msgid "Humans are allowed to send me an email (e.g. for ticket vouchers)" msgid "Humans are allowed to send me an email (e.g. for ticket vouchers)"
msgstr "Menschen dürfen mir eine E-Mail senden (z.B. für Ticket Gutscheine)" msgstr "Menschen dürfen mir eine E-Mail senden (z.B. für Ticket Gutscheine)"

View File

@ -119,3 +119,12 @@ msgstr "Question added successfully."
msgid "question.edit.success" msgid "question.edit.success"
msgstr "Question updated successfully." msgstr "Question updated successfully."
msgid "notification.news.new"
msgstr "New news: %s"
msgid "notification.news.new.introduction"
msgstr "A new news is available: %1$s"
msgid "notification.news.new.text"
msgstr "You can watch it at %3$s"

View File

@ -1,6 +1,7 @@
{% block title %}{{ __('Hi %s,', [username]) }}{% endblock %} {% block title %}{{ __('Hi %s,', [username]) }}{% endblock %}
{% block introduction %}{{ __('here is a message for you from the %s:', [config('app_name')]) }}{% endblock %} {% block introduction %}{{ __('here is a message for you from the %s:', [config('app_name')]) }}{% endblock %}
{% block message %}{{ message|raw }}{% endblock %} {% block message %}{{ message|raw }}{% endblock %}
{% block footer %}{{ __('This email is autogenerated and has not been signed. You got this email because you are registered in the %s.', [config('app_name')]) }}{% endblock %} {% block footer %}{{ __('This email is autogenerated and has not been signed. You got this email because you are registered in the %s.', [config('app_name')]) }}{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends "emails/mail.twig" %}
{% block introduction %}
{{ __('notification.news.new.introduction', [news.title, news.text, url('/news/' ~ news.id)]) }}
{% endblock %}
{% block message %}
{{ __('notification.news.new.text', [news.title, news.text, url('/news/' ~ news.id)]) }}
{% endblock %}

View File

@ -154,8 +154,13 @@ class NewsController extends BaseController
return $this->showEdit($news); return $this->showEdit($news);
} }
$isNewNews = !$news->id;
$news->save(); $news->save();
if ($isNewNews) {
event('news.created', ['news' => $news]);
}
$this->log->info( $this->log->info(
'Updated {pinned}{type} "{news}": {text}', 'Updated {pinned}{type} "{news}": {text}',
[ [

View File

@ -119,6 +119,7 @@ class Controller extends BaseController
'type' => 'gauge', 'type' => 'gauge',
['labels' => ['type' => 'system'], 'value' => $this->stats->email('system')], ['labels' => ['type' => 'system'], 'value' => $this->stats->email('system')],
['labels' => ['type' => 'humans'], 'value' => $this->stats->email('humans')], ['labels' => ['type' => 'humans'], 'value' => $this->stats->email('humans')],
['labels' => ['type' => 'news'], 'value' => $this->stats->email('news')],
], ],
'users_working' => [ 'users_working' => [
'type' => 'gauge', 'type' => 'gauge',

View File

@ -107,6 +107,9 @@ class Stats
case 'humans': case 'humans':
$query = Settings::whereEmailHuman(true); $query = Settings::whereEmailHuman(true);
break; break;
case 'news':
$query = Settings::whereEmailNews(true);
break;
default: default:
return 0; return 0;
} }

View File

@ -0,0 +1,77 @@
<?php
namespace Engelsystem\Events\Listener;
use Engelsystem\Mail\EngelsystemMailer;
use Engelsystem\Models\News as NewsModel;
use Engelsystem\Models\User\Settings as UserSettings;
use Engelsystem\Models\User\User;
use Illuminate\Database\Eloquent\Collection;
use Psr\Log\LoggerInterface;
use Swift_SwiftException as SwiftException;
class News
{
/** @var LoggerInterface */
protected $log;
/** @var EngelsystemMailer */
protected $mailer;
/** @var UserSettings */
protected $settings;
/**
* @param LoggerInterface $log
* @param EngelsystemMailer $mailer
* @param UserSettings $settings
*/
public function __construct(
LoggerInterface $log,
EngelsystemMailer $mailer,
UserSettings $settings
) {
$this->log = $log;
$this->mailer = $mailer;
$this->settings = $settings;
}
/**
* @param NewsModel $news
*/
public function created(NewsModel $news)
{
/** @var UserSettings[]|Collection $recipients */
$recipients = $this->settings
->whereEmailNews(true)
->with('user')
->get();
foreach ($recipients as $recipient) {
$this->sendMail($news, $recipient->user, 'notification.news.new', 'emails/news-new');
}
}
/**
* @param NewsModel $news
* @param User $user
* @param string $subject
* @param string $template
*/
protected function sendMail(NewsModel $news, User $user, string $subject, string $template)
{
try {
$this->mailer->sendViewTranslated(
$user,
$subject,
$template,
['title' => $news->title, 'news' => $news, 'username' => $user->name]
);
} catch (SwiftException $e) {
$this->log->error(
'Unable to send email "{title}" to user {user} with {exception}',
['title' => $subject, 'user' => $user->name, 'exception' => $e]
);
}
}
}

View File

@ -61,7 +61,7 @@ class EngelsystemMailer extends Mailer
$this->translation->setLocale($locale); $this->translation->setLocale($locale);
} }
$subject = $this->translation ? $this->translation->translate($subject) : $subject; $subject = $this->translation ? $this->translation->translate($subject, $data) : $subject;
$sentMails = $this->sendView($to, $subject, $template, $data); $sentMails = $this->sendView($to, $subject, $template, $data);
if ($activeLocale) { if ($activeLocale) {

View File

@ -9,11 +9,13 @@ use Illuminate\Database\Query\Builder as QueryBuilder;
* @property int $theme * @property int $theme
* @property bool $email_human * @property bool $email_human
* @property bool $email_shiftinfo * @property bool $email_shiftinfo
* @property bool $email_news
* *
* @method static QueryBuilder|Settings[] whereLanguage($value) * @method static QueryBuilder|Settings[] whereLanguage($value)
* @method static QueryBuilder|Settings[] whereTheme($value) * @method static QueryBuilder|Settings[] whereTheme($value)
* @method static QueryBuilder|Settings[] whereEmailHuman($value) * @method static QueryBuilder|Settings[] whereEmailHuman($value)
* @method static QueryBuilder|Settings[] whereEmailShiftinfo($value) * @method static QueryBuilder|Settings[] whereEmailShiftinfo($value)
* @method static QueryBuilder|Settings[] whereEmailNews($value)
*/ */
class Settings extends HasUserModel class Settings extends HasUserModel
{ {
@ -27,5 +29,6 @@ class Settings extends HasUserModel
'theme', 'theme',
'email_human', 'email_human',
'email_shiftinfo', 'email_shiftinfo',
'email_news',
]; ];
} }

View File

@ -2,6 +2,7 @@
use Engelsystem\Application; use Engelsystem\Application;
use Engelsystem\Config\Config; use Engelsystem\Config\Config;
use Engelsystem\Events\EventDispatcher;
use Engelsystem\Helpers\Authenticator; use Engelsystem\Helpers\Authenticator;
use Engelsystem\Helpers\Translation\Translator; use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Http\Redirector; use Engelsystem\Http\Redirector;
@ -89,6 +90,24 @@ function config_path($path = ''): string
return app('path.config') . (empty($path) ? '' : DIRECTORY_SEPARATOR . $path); return app('path.config') . (empty($path) ? '' : DIRECTORY_SEPARATOR . $path);
} }
/**
* @param string|object|null $event
* @param array $payload
*
* @return EventDispatcher
*/
function event($event = null, $payload = [])
{
/** @var EventDispatcher $dispatcher */
$dispatcher = app('events.dispatcher');
if (!is_null($event)) {
return $dispatcher->dispatch($event, $payload);
}
return $dispatcher;
}
/** /**
* @param string $path * @param string $path
* @param int $status * @param int $status

View File

@ -3,6 +3,7 @@
namespace Engelsystem\Test\Unit\Controllers\Admin; namespace Engelsystem\Test\Unit\Controllers\Admin;
use Engelsystem\Controllers\Admin\NewsController; use Engelsystem\Controllers\Admin\NewsController;
use Engelsystem\Events\EventDispatcher;
use Engelsystem\Helpers\Authenticator; use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\ValidationException; use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Validation\Validator; use Engelsystem\Http\Validation\Validator;
@ -307,6 +308,9 @@ class NewsControllerTest extends ControllerTest
$this->auth = $this->createMock(Authenticator::class); $this->auth = $this->createMock(Authenticator::class);
$this->app->instance(Authenticator::class, $this->auth); $this->app->instance(Authenticator::class, $this->auth);
$eventDispatcher = $this->createMock(EventDispatcher::class);
$this->app->instance('events.dispatcher', $eventDispatcher);
(new News([ (new News([
'title' => 'Foo', 'title' => 'Foo',
'text' => '<b>foo</b>', 'text' => '<b>foo</b>',

View File

@ -249,6 +249,7 @@ class StatsTest extends TestCase
$this->assertEquals(0, $stats->email('not-available-option')); $this->assertEquals(0, $stats->email('not-available-option'));
$this->assertEquals(2, $stats->email('system')); $this->assertEquals(2, $stats->email('system'));
$this->assertEquals(3, $stats->email('humans')); $this->assertEquals(3, $stats->email('humans'));
$this->assertEquals(1, $stats->email('news'));
} }
/** /**
@ -378,7 +379,7 @@ class StatsTest extends TestCase
{ {
$this->addUser(); $this->addUser();
$this->addUser([], ['shirt_size' => 'L'], ['email_human' => true, 'email_shiftinfo' => true]); $this->addUser([], ['shirt_size' => 'L'], ['email_human' => true, 'email_shiftinfo' => true]);
$this->addUser(['arrived' => 1], [], ['email_human' => true]); $this->addUser(['arrived' => 1], [], ['email_human' => true, 'email_news' => true]);
$this->addUser(['arrived' => 1], [], ['language' => 'lo_RM', 'email_shiftinfo' => true]); $this->addUser(['arrived' => 1], [], ['language' => 'lo_RM', 'email_shiftinfo' => true]);
$this->addUser(['arrived' => 1, 'got_voucher' => 2], ['shirt_size' => 'XXL'], ['language' => 'lo_RM']); $this->addUser(['arrived' => 1, 'got_voucher' => 2], ['shirt_size' => 'XXL'], ['language' => 'lo_RM']);
$this->addUser(['arrived' => 1, 'got_voucher' => 9, 'force_active' => true], [], ['theme' => 1]); $this->addUser(['arrived' => 1, 'got_voucher' => 9, 'force_active' => true], [], ['theme' => 1]);

View File

@ -0,0 +1,101 @@
<?php
namespace Engelsystem\Test\Unit\Events\Listener;
use Engelsystem\Events\Listener\News;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Mail\EngelsystemMailer;
use Engelsystem\Models\News as NewsModel;
use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Psr\Log\Test\TestLogger;
use Swift_SwiftException as SwiftException;
class NewsTest extends TestCase
{
use HasDatabase;
/** @var TestLogger */
protected $log;
/** @var EngelsystemMailer|MockObject */
protected $mailer;
/** @var User */
protected $user;
/**
* @covers \Engelsystem\Events\Listener\News::created
* @covers \Engelsystem\Events\Listener\News::__construct
* @covers \Engelsystem\Events\Listener\News::sendMail
*/
public function testCreated()
{
$news = new NewsModel([
'title' => 'Foo',
'text' => 'Bar',
'user_id' => 1,
]);
$news->save();
$i = 0;
$this->mailer->expects($this->exactly(2))
->method('sendViewTranslated')
->willReturnCallback(function (User $user, string $subject, string $template, array $data) use (&$i) {
$this->assertEquals(1, $user->id);
$this->assertEquals('notification.news.new', $subject);
$this->assertEquals('emails/news-new', $template);
$this->assertEquals('Foo', array_values($data)[0]);
if ($i++ > 0) {
throw new SwiftException('Oops');
}
return 1;
});
/** @var News $listener */
$listener = $this->app->make(News::class);
$error = 'Unable to send email';
$listener->created($news);
$this->assertFalse($this->log->hasErrorThatContains($error));
$listener->created($news);
$this->assertTrue($this->log->hasErrorThatContains($error));
}
protected function setUp(): void
{
parent::setUp();
$this->initDatabase();
$this->log = new TestLogger();
$this->app->instance(LoggerInterface::class, $this->log);
$this->mailer = $this->createMock(EngelsystemMailer::class);
$this->app->instance(EngelsystemMailer::class, $this->mailer);
$this->user = new User([
'name' => 'test',
'password' => '',
'email' => 'foo@bar.baz',
'api_key' => '',
]);
$this->user->save();
$settings = new Settings([
'language' => '',
'theme' => 1,
'email_news' => true,
]);
$settings->user()
->associate($this->user)
->save();
}
}

View File

@ -5,6 +5,7 @@ namespace Engelsystem\Test\Unit;
use Engelsystem\Application; use Engelsystem\Application;
use Engelsystem\Config\Config; use Engelsystem\Config\Config;
use Engelsystem\Container\Container; use Engelsystem\Container\Container;
use Engelsystem\Events\EventDispatcher;
use Engelsystem\Helpers\Authenticator; use Engelsystem\Helpers\Authenticator;
use Engelsystem\Helpers\Translation\Translator; use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Http\Redirector; use Engelsystem\Http\Redirector;
@ -13,7 +14,6 @@ use Engelsystem\Http\Response;
use Engelsystem\Http\UrlGeneratorInterface; use Engelsystem\Http\UrlGeneratorInterface;
use Engelsystem\Renderer\Renderer; use Engelsystem\Renderer\Renderer;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface as StorageInterface; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface as StorageInterface;
@ -141,6 +141,28 @@ class HelpersTest extends TestCase
$this->assertEquals('/foo/conf/bar.php', config_path('bar.php')); $this->assertEquals('/foo/conf/bar.php', config_path('bar.php'));
} }
/**
* @covers \event
*/
public function testEvent()
{
/** @var Application|MockObject $app */
$app = $this->createMock(Container::class);
Application::setInstance($app);
/** @var EventDispatcher|MockObject $dispatcher */
$dispatcher = $this->createMock(EventDispatcher::class);
$this->setExpects($dispatcher, 'dispatch', ['testevent', ['some' => 'thing']], ['test']);
$app->expects($this->atLeastOnce())
->method('get')
->with('events.dispatcher')
->willReturn($dispatcher);
$this->assertEquals($dispatcher, event());
$this->assertEquals(['test'], event('testevent', ['some' => 'thing']));
}
/** /**
* @covers \redirect * @covers \redirect
*/ */

View File

@ -79,7 +79,7 @@ class EngelsystemMailerTest extends TestCase
$this->setExpects($mailer, 'sendView', ['foo@bar.baz', 'Lorem dolor', 'test/template.tpl', ['dev' => true]], 1); $this->setExpects($mailer, 'sendView', ['foo@bar.baz', 'Lorem dolor', 'test/template.tpl', ['dev' => true]], 1);
$this->setExpects($translator, 'getLocales', null, ['de_DE' => 'de_DE', 'en_US' => 'en_US']); $this->setExpects($translator, 'getLocales', null, ['de_DE' => 'de_DE', 'en_US' => 'en_US']);
$this->setExpects($translator, 'getLocale', null, 'en_US'); $this->setExpects($translator, 'getLocale', null, 'en_US');
$this->setExpects($translator, 'translate', ['translatable.text'], 'Lorem dolor'); $this->setExpects($translator, 'translate', ['translatable.text', ['dev' => true]], 'Lorem dolor');
$translator->expects($this->exactly(2)) $translator->expects($this->exactly(2))
->method('setLocale') ->method('setLocale')
->withConsecutive(['de_DE'], ['en_US']); ->withConsecutive(['de_DE'], ['en_US']);