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',
'news.created' => \Engelsystem\Events\Listener\News::class . '@created',
'news.updated' => \Engelsystem\Events\Listener\News::class . '@updated',
'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"
msgstr "Du kannst sie dir unter %3$s anschauen."
msgid "notification.news.updated"
msgstr "Aktualisierte News: %s"
msgid "notification.messages.new"
msgstr "Neue private Nachricht von %s"

View File

@ -86,6 +86,9 @@ msgstr "Bitte melde dich an."
msgid "form.submit"
msgstr "Absenden"
msgid "form.send_notification"
msgstr "Benachrichtigungen versenden"
msgid "credits.source"
msgstr "Quellcode"
@ -1513,6 +1516,12 @@ msgstr "News \"%s\" löschen"
msgid "news.comments.delete.title"
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"
msgstr "Suchen"

View File

@ -216,7 +216,10 @@ 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"
msgstr "You can view it at %3$s"
msgid "notification.news.updated"
msgstr "Updated News: %s"
msgid "notification.messages.new"
msgstr "New private message from %s"

View File

@ -22,6 +22,9 @@ msgstr "Your password is incorrect. Please try it again."
msgid "form.submit"
msgstr "Submit"
msgid "form.send_notification"
msgstr "Send notifications"
msgid "general.login"
msgstr "Login"
@ -244,6 +247,12 @@ msgstr "Delete news \"%s\""
msgid "news.comments.delete.title"
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"
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 %}
{{ f.delete(__('form.delete'), {'confirm_title': __('news.delete.title', [news.title[:40]|e])}) }}
{% endif %}
{{ f.checkbox('send_notification', __('form.send_notification'), {'checked': send_notification, 'class': 'ms-2 form-check-inline'}) }}
</div>
</div>

View File

@ -39,10 +39,10 @@ class NewsController extends BaseController
$news = $this->news->find($newsId);
$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(
'pages/news/edit.twig',
@ -51,6 +51,7 @@ class NewsController extends BaseController
'is_meeting' => $news ? $news->is_meeting : $isMeetingDefault,
'is_pinned' => $news ? $news->is_pinned : false,
'is_highlighted' => $news ? $news->is_highlighted : false,
'send_notification' => $sendNotification,
],
);
}
@ -86,6 +87,7 @@ class NewsController extends BaseController
'is_highlighted' => 'optional|checked',
'delete' => 'optional|checked',
'preview' => 'optional|checked',
'send_notification' => 'optional|checked',
]);
if (!$news->user) {
@ -95,24 +97,27 @@ class NewsController extends BaseController
$news->text = $data['text'];
$news->is_meeting = !is_null($data['is_meeting']);
$news->is_pinned = !is_null($data['is_pinned']);
$notify = !is_null($data['send_notification']);
if ($this->auth->can('news.highlight')) {
$news->is_highlighted = !is_null($data['is_highlighted']);
}
if (!is_null($data['preview'])) {
return $this->showEdit($news);
return $this->showEdit($news, $notify);
}
$isNewNews = !$news->id;
if ($isNewNews && News::where('title', $news->title)->where('text', $news->text)->count()) {
$this->addNotification('news.edit.duplicate', NotificationType::ERROR);
return $this->showEdit($news);
return $this->showEdit($news, $notify);
}
$news->save();
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(

View File

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

View File

@ -22,7 +22,9 @@ class NewsTest extends TestCase
protected TestLogger $log;
protected EngelsystemMailer|MockObject $mailer;
protected EngelsystemMailer | MockObject $mailer;
protected NewsModel $news;
protected User $user;
@ -33,14 +35,10 @@ class NewsTest extends TestCase
*/
public function testCreated(): void
{
$this->app->instance('config', new Config());
/** @var NewsModel $news */
$news = NewsModel::factory(['title' => 'Foo'])->create();
$this->mailer->expects($this->once())
->method('sendViewTranslated')
->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('emails/news-new', $template);
$this->assertEquals('Foo', array_values($data)[0]);
@ -50,7 +48,55 @@ class NewsTest extends TestCase
/** @var News $listener */
$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
@ -64,6 +110,10 @@ class NewsTest extends TestCase
$this->mailer = $this->createMock(EngelsystemMailer::class);
$this->app->instance(EngelsystemMailer::class, $this->mailer);
$this->app->instance('config', new Config());
$this->news = NewsModel::factory(['title' => 'Foo'])->create();
$this->user = User::factory()
->has(Settings::factory([
'language' => '',