Send notifications on news updates, make notifications optional

This commit is contained in:
Igor Scheller 2023-12-21 13:41:10 +01:00 committed by msquare
parent 4429516a22
commit 7888dfad78
11 changed files with 161 additions and 38 deletions

View File

@ -76,6 +76,7 @@ return [
'message.created' => \Engelsystem\Events\Listener\Messages::class . '@created', 'message.created' => \Engelsystem\Events\Listener\Messages::class . '@created',
'news.created' => \Engelsystem\Events\Listener\News::class . '@created', 'news.created' => \Engelsystem\Events\Listener\News::class . '@created',
'news.updated' => \Engelsystem\Events\Listener\News::class . '@updated',
'oauth2.login' => \Engelsystem\Events\Listener\OAuth2::class . '@login', 'oauth2.login' => \Engelsystem\Events\Listener\OAuth2::class . '@login',

View File

@ -219,6 +219,9 @@ msgstr "Es gibt eine neue News: %1$s"
msgid "notification.news.new.text" msgid "notification.news.new.text"
msgstr "Du kannst sie dir unter %3$s anschauen." msgstr "Du kannst sie dir unter %3$s anschauen."
msgid "notification.news.updated"
msgstr "Aktualisierte News: %s"
msgid "notification.messages.new" msgid "notification.messages.new"
msgstr "Neue private Nachricht von %s" msgstr "Neue private Nachricht von %s"

View File

@ -86,6 +86,9 @@ msgstr "Bitte melde dich an."
msgid "form.submit" msgid "form.submit"
msgstr "Absenden" msgstr "Absenden"
msgid "form.send_notification"
msgstr "Benachrichtigungen versenden"
msgid "credits.source" msgid "credits.source"
msgstr "Quellcode" msgstr "Quellcode"
@ -1513,6 +1516,12 @@ msgstr "News \"%s\" löschen"
msgid "news.comments.delete.title" msgid "news.comments.delete.title"
msgstr "Kommentar \"%s\" löschen" msgstr "Kommentar \"%s\" löschen"
msgid "notification.news.updated.introduction"
msgstr "Die News %1$s wurde aktualisiert"
msgid "notification.news.updated.text"
msgstr "Du kannst sie dir unter %3$s anschauen."
msgid "form.search" msgid "form.search"
msgstr "Suchen" msgstr "Suchen"

View File

@ -216,7 +216,10 @@ msgid "notification.news.new.introduction"
msgstr "A new news is available: %1$s" msgstr "A new news is available: %1$s"
msgid "notification.news.new.text" msgid "notification.news.new.text"
msgstr "You can watch it at %3$s" msgstr "You can view it at %3$s"
msgid "notification.news.updated"
msgstr "Updated News: %s"
msgid "notification.messages.new" msgid "notification.messages.new"
msgstr "New private message from %s" msgstr "New private message from %s"

View File

@ -22,6 +22,9 @@ msgstr "Your password is incorrect. Please try it again."
msgid "form.submit" msgid "form.submit"
msgstr "Submit" msgstr "Submit"
msgid "form.send_notification"
msgstr "Send notifications"
msgid "general.login" msgid "general.login"
msgstr "Login" msgstr "Login"
@ -244,6 +247,12 @@ msgstr "Delete news \"%s\""
msgid "news.comments.delete.title" msgid "news.comments.delete.title"
msgstr "Delete comment \"%s\"" msgstr "Delete comment \"%s\""
msgid "notification.news.updated.introduction"
msgstr "The news %1$s was updated"
msgid "notification.news.updated.text"
msgstr "You can view it at %3$s"
msgid "form.search" msgid "form.search"
msgstr "Search" msgstr "Search"

View File

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

View File

@ -77,6 +77,8 @@
{% if news and news.id %} {% if news and news.id %}
{{ f.delete(__('form.delete'), {'confirm_title': __('news.delete.title', [news.title[:40]|e])}) }} {{ f.delete(__('form.delete'), {'confirm_title': __('news.delete.title', [news.title[:40]|e])}) }}
{% endif %} {% endif %}
{{ f.checkbox('send_notification', __('form.send_notification'), {'checked': send_notification, 'class': 'ms-2 form-check-inline'}) }}
</div> </div>
</div> </div>

View File

@ -39,10 +39,10 @@ class NewsController extends BaseController
$news = $this->news->find($newsId); $news = $this->news->find($newsId);
$isMeeting = (bool) $request->get('meeting', false); $isMeeting = (bool) $request->get('meeting', false);
return $this->showEdit($news, $isMeeting); return $this->showEdit($news, true, $isMeeting);
} }
protected function showEdit(?News $news, bool $isMeetingDefault = false): Response protected function showEdit(?News $news, bool $sendNotification = true, bool $isMeetingDefault = false): Response
{ {
return $this->response->withView( return $this->response->withView(
'pages/news/edit.twig', 'pages/news/edit.twig',
@ -51,6 +51,7 @@ class NewsController extends BaseController
'is_meeting' => $news ? $news->is_meeting : $isMeetingDefault, 'is_meeting' => $news ? $news->is_meeting : $isMeetingDefault,
'is_pinned' => $news ? $news->is_pinned : false, 'is_pinned' => $news ? $news->is_pinned : false,
'is_highlighted' => $news ? $news->is_highlighted : false, 'is_highlighted' => $news ? $news->is_highlighted : false,
'send_notification' => $sendNotification,
], ],
); );
} }
@ -86,6 +87,7 @@ class NewsController extends BaseController
'is_highlighted' => 'optional|checked', 'is_highlighted' => 'optional|checked',
'delete' => 'optional|checked', 'delete' => 'optional|checked',
'preview' => 'optional|checked', 'preview' => 'optional|checked',
'send_notification' => 'optional|checked',
]); ]);
if (!$news->user) { if (!$news->user) {
@ -95,24 +97,27 @@ class NewsController extends BaseController
$news->text = $data['text']; $news->text = $data['text'];
$news->is_meeting = !is_null($data['is_meeting']); $news->is_meeting = !is_null($data['is_meeting']);
$news->is_pinned = !is_null($data['is_pinned']); $news->is_pinned = !is_null($data['is_pinned']);
$notify = !is_null($data['send_notification']);
if ($this->auth->can('news.highlight')) { if ($this->auth->can('news.highlight')) {
$news->is_highlighted = !is_null($data['is_highlighted']); $news->is_highlighted = !is_null($data['is_highlighted']);
} }
if (!is_null($data['preview'])) { if (!is_null($data['preview'])) {
return $this->showEdit($news); return $this->showEdit($news, $notify);
} }
$isNewNews = !$news->id; $isNewNews = !$news->id;
if ($isNewNews && News::where('title', $news->title)->where('text', $news->text)->count()) { if ($isNewNews && News::where('title', $news->title)->where('text', $news->text)->count()) {
$this->addNotification('news.edit.duplicate', NotificationType::ERROR); $this->addNotification('news.edit.duplicate', NotificationType::ERROR);
return $this->showEdit($news); return $this->showEdit($news, $notify);
} }
$news->save(); $news->save();
if ($isNewNews) { if ($isNewNews) {
event('news.created', ['news' => $news]); event('news.created', ['news' => $news, 'sendNotification' => $notify]);
} else {
event('news.updated', ['news' => $news, 'sendNotification' => $notify]);
} }
$this->log->info( $this->log->info(

View File

@ -7,7 +7,6 @@ namespace Engelsystem\Events\Listener;
use Engelsystem\Mail\EngelsystemMailer; use Engelsystem\Mail\EngelsystemMailer;
use Engelsystem\Models\News as NewsModel; use Engelsystem\Models\News as NewsModel;
use Engelsystem\Models\User\Settings as UserSettings; use Engelsystem\Models\User\Settings as UserSettings;
use Engelsystem\Models\User\User;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -20,26 +19,35 @@ class News
) { ) {
} }
public function created(NewsModel $news): void public function created(NewsModel $news, bool $sendNotification = true): void
{ {
$this->sendMail($news, 'notification.news.new', 'emails/news-new', $sendNotification);
}
public function updated(NewsModel $news, bool $sendNotification = true): void
{
$this->sendMail($news, 'notification.news.updated', 'emails/news-updated', $sendNotification);
}
protected function sendMail(NewsModel $news, string $subject, string $template, bool $sendNotification = true): void
{
if (!$sendNotification) {
return;
}
/** @var UserSettings[]|Collection $recipients */ /** @var UserSettings[]|Collection $recipients */
$recipients = $this->settings $recipients = $this->settings
->whereEmailNews(true) ->with('user.personalData')
->with('user') ->where('email_news', true)
->get(); ->get();
foreach ($recipients as $recipient) { foreach ($recipients as $recipient) {
$this->sendMail($news, $recipient->user, 'notification.news.new', 'emails/news-new'); $this->mailer->sendViewTranslated(
$recipient->user,
$subject,
$template,
['title' => $news->title, 'news' => $news, 'username' => $recipient->user->displayName]
);
} }
} }
protected function sendMail(NewsModel $news, User $user, string $subject, string $template): void
{
$this->mailer->sendViewTranslated(
$user,
$subject,
$template,
['title' => $news->title, 'news' => $news, 'username' => $user->displayName]
);
}
} }

View File

@ -11,6 +11,7 @@ use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\ValidationException; use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Validation\Validator; use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\News; use Engelsystem\Models\News;
use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\User; use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\Controllers\ControllerTest; use Engelsystem\Test\Unit\Controllers\ControllerTest;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
@ -18,6 +19,7 @@ use PHPUnit\Framework\MockObject\MockObject;
class NewsControllerTest extends ControllerTest class NewsControllerTest extends ControllerTest
{ {
protected Authenticator|MockObject $auth; protected Authenticator|MockObject $auth;
protected EventDispatcher|MockObject $eventDispatcher;
/** @var array */ /** @var array */
protected array $data = [ protected array $data = [
@ -42,6 +44,7 @@ class NewsControllerTest extends ControllerTest
$this->assertEquals('pages/news/edit.twig', $view); $this->assertEquals('pages/news/edit.twig', $view);
$this->assertNotEmpty($data['news']); $this->assertNotEmpty($data['news']);
$this->assertTrue($data['send_notification']);
return $this->response; return $this->response;
}); });
@ -101,10 +104,13 @@ class NewsControllerTest extends ControllerTest
public function saveCreateEditProvider(): array public function saveCreateEditProvider(): array
{ {
return [ return [
// Text, isMeeting, id, sendNotification
['Some test', true], ['Some test', true],
['Some test', false], ['Some test', false],
['Some test', false, 1], ['Some test', false, 1],
['Some test', true, 1], ['Some test', true, 1],
['Some test', false, null, true],
['Some test', false, 1, true],
]; ];
} }
@ -117,20 +123,33 @@ class NewsControllerTest extends ControllerTest
public function testSaveCreateEdit( public function testSaveCreateEdit(
string $text, string $text,
bool $isMeeting, bool $isMeeting,
int $id = null int $id = null,
bool $sendNotification = false
): void { ): void {
$this->request->attributes->set('news_id', $id); $this->request->attributes->set('news_id', $id);
$id = $id ?: 2;
$body = [ $body = [
'title' => 'Some Title', 'title' => 'Some Title',
'text' => $text, 'text' => $text,
]; ];
if ($isMeeting) { if ($isMeeting) {
$body['is_meeting'] = '1'; $body['is_meeting'] = '1';
} }
if ($sendNotification) {
$body['send_notification'] = '1';
}
$this->eventDispatcher->expects($this->once())
->method('dispatch')
->willReturnCallback(function (string $event, array $payload) use ($id, $sendNotification) {
$this->assertEquals($id ? 'news.updated' : 'news.created', $event);
$this->assertEquals($sendNotification, $payload['sendNotification']);
$this->assertInstanceOf(News::class, $payload['news']);
return $this->eventDispatcher;
});
$this->request = $this->request->withParsedBody($body);
$this->addUser(); $this->addUser();
$this->request = $this->request->withParsedBody($body);
$this->response->expects($this->once()) $this->response->expects($this->once())
->method('redirectTo') ->method('redirectTo')
->with('http://localhost/news') ->with('http://localhost/news')
@ -146,7 +165,7 @@ class NewsControllerTest extends ControllerTest
$this->assertHasNotification('news.edit.success'); $this->assertHasNotification('news.edit.success');
$news = (new News())->find($id); $news = (new News())->find($id ?: 2);
$this->assertEquals($text, $news->text); $this->assertEquals($text, $news->text);
$this->assertEquals($isMeeting, (bool) $news->is_meeting); $this->assertEquals($isMeeting, (bool) $news->is_meeting);
} }
@ -164,6 +183,7 @@ class NewsControllerTest extends ControllerTest
'is_pinned' => '1', 'is_pinned' => '1',
'is_highlighted' => '1', 'is_highlighted' => '1',
'preview' => '1', 'preview' => '1',
'send_notification' => '1',
]); ]);
$this->response->expects($this->once()) $this->response->expects($this->once())
->method('withView') ->method('withView')
@ -179,6 +199,8 @@ class NewsControllerTest extends ControllerTest
$this->assertEquals('New title', $news->title); $this->assertEquals('New title', $news->title);
$this->assertEquals('New text', $news->text); $this->assertEquals('New text', $news->text);
$this->assertTrue($data['send_notification']);
return $this->response; return $this->response;
}); });
$this->auth->expects($this->atLeastOnce()) $this->auth->expects($this->atLeastOnce())
@ -256,7 +278,9 @@ class NewsControllerTest extends ControllerTest
*/ */
protected function addUser(): void protected function addUser(): void
{ {
$user = User::factory(['id' => 42])->create(); $user = User::factory(['id' => 42])
->has(Settings::factory(['email_news' => true]))
->create();
$this->auth->expects($this->any()) $this->auth->expects($this->any())
->method('user') ->method('user')
@ -273,11 +297,11 @@ 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->eventDispatcher = $this->createMock(EventDispatcher::class);
$eventDispatcher->expects(self::any()) $this->eventDispatcher->expects(self::any())
->method('dispatch') ->method('dispatch')
->willReturnSelf(); ->willReturnSelf();
$this->app->instance('events.dispatcher', $eventDispatcher); $this->app->instance('events.dispatcher', $this->eventDispatcher);
$user = User::factory()->create(); $user = User::factory()->create();
(new News([ (new News([

View File

@ -22,7 +22,9 @@ class NewsTest extends TestCase
protected TestLogger $log; protected TestLogger $log;
protected EngelsystemMailer|MockObject $mailer; protected EngelsystemMailer | MockObject $mailer;
protected NewsModel $news;
protected User $user; protected User $user;
@ -33,14 +35,10 @@ class NewsTest extends TestCase
*/ */
public function testCreated(): void public function testCreated(): void
{ {
$this->app->instance('config', new Config());
/** @var NewsModel $news */
$news = NewsModel::factory(['title' => 'Foo'])->create();
$this->mailer->expects($this->once()) $this->mailer->expects($this->once())
->method('sendViewTranslated') ->method('sendViewTranslated')
->willReturnCallback(function (User $user, string $subject, string $template, array $data): bool { ->willReturnCallback(function (User $user, string $subject, string $template, array $data): bool {
$this->assertEquals(1, $user->id); $this->assertEquals($this->user->id, $user->id);
$this->assertEquals('notification.news.new', $subject); $this->assertEquals('notification.news.new', $subject);
$this->assertEquals('emails/news-new', $template); $this->assertEquals('emails/news-new', $template);
$this->assertEquals('Foo', array_values($data)[0]); $this->assertEquals('Foo', array_values($data)[0]);
@ -50,7 +48,55 @@ class NewsTest extends TestCase
/** @var News $listener */ /** @var News $listener */
$listener = $this->app->make(News::class); $listener = $this->app->make(News::class);
$listener->created($news); $listener->created($this->news);
}
/**
* @covers \Engelsystem\Events\Listener\News::created
* @covers \Engelsystem\Events\Listener\News::sendMail
*/
public function testCreatedNoNotification(): void
{
$this->setExpects($this->mailer, 'sendViewTranslated', null, null, $this->never());
/** @var News $listener */
$listener = $this->app->make(News::class);
$listener->created($this->news, false);
}
/**
* @covers \Engelsystem\Events\Listener\News::updated
* @covers \Engelsystem\Events\Listener\News::sendMail
*/
public function testUpdated(): void
{
$this->mailer->expects($this->once())
->method('sendViewTranslated')
->willReturnCallback(function (User $user, string $subject, string $template, array $data): bool {
$this->assertEquals($this->user->id, $user->id);
$this->assertEquals('notification.news.updated', $subject);
$this->assertEquals('emails/news-updated', $template);
$this->assertEquals('Foo', array_values($data)[0]);
return true;
});
/** @var News $listener */
$listener = $this->app->make(News::class);
$listener->updated($this->news);
}
/**
* @covers \Engelsystem\Events\Listener\News::updated
* @covers \Engelsystem\Events\Listener\News::sendMail
*/
public function testUpdatedNoNotification(): void
{
$this->setExpects($this->mailer, 'sendViewTranslated', null, null, $this->never());
/** @var News $listener */
$listener = $this->app->make(News::class);
$listener->updated($this->news, false);
} }
protected function setUp(): void protected function setUp(): void
@ -64,6 +110,10 @@ class NewsTest extends TestCase
$this->mailer = $this->createMock(EngelsystemMailer::class); $this->mailer = $this->createMock(EngelsystemMailer::class);
$this->app->instance(EngelsystemMailer::class, $this->mailer); $this->app->instance(EngelsystemMailer::class, $this->mailer);
$this->app->instance('config', new Config());
$this->news = NewsModel::factory(['title' => 'Foo'])->create();
$this->user = User::factory() $this->user = User::factory()
->has(Settings::factory([ ->has(Settings::factory([
'language' => '', 'language' => '',