diff --git a/config/app.php b/config/app.php index 565fddb1..65179096 100644 --- a/config/app.php +++ b/config/app.php @@ -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', diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po index d29a780e..eda8a898 100644 --- a/resources/lang/de_DE/additional.po +++ b/resources/lang/de_DE/additional.po @@ -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" diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index 4fb9b241..a3de9fae 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -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" diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po index c828af52..6ac5e09f 100644 --- a/resources/lang/en_US/additional.po +++ b/resources/lang/en_US/additional.po @@ -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" diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index ed632a5a..fd96a2ed 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -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" diff --git a/resources/views/emails/news-updated.twig b/resources/views/emails/news-updated.twig new file mode 100644 index 00000000..dd51f5f6 --- /dev/null +++ b/resources/views/emails/news-updated.twig @@ -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 %} diff --git a/resources/views/pages/news/edit.twig b/resources/views/pages/news/edit.twig index c6ba285e..639dd9d7 100644 --- a/resources/views/pages/news/edit.twig +++ b/resources/views/pages/news/edit.twig @@ -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'}) }} diff --git a/src/Controllers/Admin/NewsController.php b/src/Controllers/Admin/NewsController.php index c91dd8aa..c6fdeb5a 100644 --- a/src/Controllers/Admin/NewsController.php +++ b/src/Controllers/Admin/NewsController.php @@ -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( diff --git a/src/Events/Listener/News.php b/src/Events/Listener/News.php index 8be208cb..72836f35 100644 --- a/src/Events/Listener/News.php +++ b/src/Events/Listener/News.php @@ -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] - ); - } } diff --git a/tests/Unit/Controllers/Admin/NewsControllerTest.php b/tests/Unit/Controllers/Admin/NewsControllerTest.php index 95b5277d..8f466938 100644 --- a/tests/Unit/Controllers/Admin/NewsControllerTest.php +++ b/tests/Unit/Controllers/Admin/NewsControllerTest.php @@ -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([ diff --git a/tests/Unit/Events/Listener/NewsTest.php b/tests/Unit/Events/Listener/NewsTest.php index 8b2388b8..ff22be83 100644 --- a/tests/Unit/Events/Listener/NewsTest.php +++ b/tests/Unit/Events/Listener/NewsTest.php @@ -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' => '',