Resolve "Notification: envoi à des groupes utilisateurs"

This commit is contained in:
LenaertsJ 2025-07-20 20:18:49 +00:00 committed by Julien Fastré
parent 5bdb2df929
commit ab8da4ab7a
47 changed files with 1635 additions and 148 deletions

View File

@ -185,14 +185,57 @@ When we need to use a DateTime or DateTimeImmutable that need to express "now",
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
where injection does not work when restoring an entity from database, but usually possible in services.
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
where we have full and easy control of the date.
### Testing Information
The project uses PHPUnit for testing. Each bundle has its own test suite, and there's also a global test suite at the root level.
#### Use of mock in tests
##### General mocking
For creating mock, we prefer using prophecy (library phpspec/prophecy).
##### Useful helpers and tips that avoid create a mock
Some notable implementations that are tests helper, and avoid to create a mock:
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
- `\Symfony\Component\HttpClient\MockHttpClient`, an implementation of `\Symfony\Contracts\HttpClient\HttpClientInterface`;
- When using `\Symfony\Component\Mailer\MailerInterface`, we can create the mock with "InMemoryTransport":
```php
use Symfony\Component\Mailer\Transport\InMemoryTransport;
use \Symfony\Component\Mailer\Mailer;
$transport = new InMemoryTransport();
$mailer = new Mailer($transport);
// After sending:
$messages = $transport->getSent(); // array of SentMessage
```
- When using `\Symfony\Contracts\EventDispatcher\EventDispatcherInterface`, we can use directly an instance of `\Symfony\Component\EventDispatcher\EventDispatcher`;
##### When we prefer not creating a mock
- When we use Doctrine Entities related to the project, we prefer not to use a mock: we instantiate them directly (unless it requires too much code to write);
##### Mocking final and readonly classes
Classes marked as final can't be mocked. To avoid that, either:
- we remove the `final` keyword from the class;
- we extract an interface from the final class.
This must be a decision made by a human, not by an AI. Every AI task must abort with an explicit message in that case.
#### Running Tests
The tests are run from the project's root (not from the bundle's root).
```bash
# Run all tests
vendor/bin/phpunit

View File

@ -62,8 +62,10 @@ framework:
'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority
'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async
'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage': async
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async
# end of routes added by chill-bundles recipes
# Route your messages to the transports
# 'App\Message\YourMessage': async

View File

@ -24,7 +24,11 @@ use Doctrine\ORM\EntityManagerInterface;
class CalendarForShortMessageProvider
{
public function __construct(private readonly CalendarRepository $calendarRepository, private readonly EntityManagerInterface $em, private readonly RangeGeneratorInterface $rangeGenerator) {}
public function __construct(
private readonly CalendarRepository $calendarRepository,
private readonly EntityManagerInterface $em,
private readonly RangeGeneratorInterface $rangeGenerator,
) {}
/**
* Generate calendars instance.

View File

@ -21,7 +21,6 @@ namespace Chill\CalendarBundle\Tests\Service\ShortMessageNotification;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Repository\CalendarRepository;
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator;
use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
@ -82,10 +81,16 @@ final class CalendarForShortMessageProviderTest extends TestCase
$em = $this->prophesize(EntityManagerInterface::class);
$em->clear()->shouldBeCalled();
$calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
$calendarRangeGenerator->generateRange(Argument::any())->willReturn([
'startDate' => new \DateTimeImmutable('yesterday'),
'endDate' => new \DateTimeImmutable('now'),
]);
$provider = new CalendarForShortMessageProvider(
$calendarRepository->reveal(),
$em->reveal(),
new DefaultRangeGenerator()
$calendarRangeGenerator->reveal(),
);
$calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
@ -103,26 +108,32 @@ final class CalendarForShortMessageProviderTest extends TestCase
Argument::type(\DateTimeImmutable::class),
Argument::type('int'),
Argument::exact(0)
)->will(static fn ($args) => array_fill(0, 1, new Calendar()))->shouldBeCalledTimes(1);
)->will(static fn ($args) => array_fill(0, 10, new Calendar()))->shouldBeCalledTimes(1);
$calendarRepository->findByNotificationAvailable(
Argument::type(\DateTimeImmutable::class),
Argument::type(\DateTimeImmutable::class),
Argument::type('int'),
Argument::not(0)
Argument::exact(10)
)->will(static fn ($args) => [])->shouldBeCalledTimes(1);
$em = $this->prophesize(EntityManagerInterface::class);
$em->clear()->shouldBeCalled();
$calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
$calendarRangeGenerator->generateRange(Argument::any())->willReturn([
'startDate' => new \DateTimeImmutable('yesterday'),
'endDate' => new \DateTimeImmutable('now'),
]);
$provider = new CalendarForShortMessageProvider(
$calendarRepository->reveal(),
$em->reveal(),
new DefaultRangeGenerator()
$calendarRangeGenerator->reveal(),
);
$calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
$this->assertEquals(1, \count($calendars));
$this->assertEquals(10, \count($calendars));
$this->assertContainsOnly(Calendar::class, $calendars);
}
}

View File

@ -20,6 +20,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompiler
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
use Chill\MainBundle\Notification\NotificationHandlerInterface;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Search\SearchApiInterface;
@ -61,6 +62,8 @@ class ChillMainBundle extends Bundle
->addTag('chill_main.entity_info_provider');
$container->registerForAutoconfiguration(ProvideRoleInterface::class)
->addTag('chill_main.provide_role');
$container->registerForAutoconfiguration(NotificationFlagProviderInterface::class)
->addTag('chill_main.notification_flag_provider');
$container->addCompilerPass(new SearchableServicesCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ConfigConsistencyCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);

View File

@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Form\NotificationCommentType;
use Chill\MainBundle\Form\NotificationType;
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
use Chill\MainBundle\Notification\FlagProviders\NotificationByUserFlagProvider;
use Chill\MainBundle\Notification\NotificationHandlerManager;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NotificationRepository;
@ -57,7 +58,8 @@ class NotificationController extends AbstractController
$notification
->setRelatedEntityClass($request->query->get('entityClass'))
->setRelatedEntityId($request->query->getInt('entityId'))
->setSender($this->security->getUser());
->setSender($this->security->getUser())
->setType(NotificationByUserFlagProvider::FLAG);
$tos = $request->query->all('tos');

View File

@ -11,14 +11,11 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Form\UserPhonenumberType;
use Chill\MainBundle\Form\UserProfileType;
use Chill\MainBundle\Security\ChillSecurity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Routing\Annotation\Route;
@ -41,16 +38,19 @@ final class UserProfileController extends AbstractController
}
$user = $this->security->getUser();
$editForm = $this->createPhonenumberEditForm($user);
$editForm = $this->createForm(UserProfileType::class, $user);
$editForm->get('notificationFlags')->setData($user->getNotificationFlags());
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$phonenumber = $editForm->get('phonenumber')->getData();
$notificationFlagsData = $editForm->get('notificationFlags')->getData();
$user->setNotificationFlags($notificationFlagsData);
$user->setPhonenumber($phonenumber);
$this->managerRegistry->getManager()->flush();
$this->addFlash('success', $this->translator->trans('user.profile.Phonenumber successfully updated!'));
$em = $this->managerRegistry->getManager();
$em->flush();
$this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!'));
return $this->redirectToRoute('chill_main_user_profile');
}
@ -60,13 +60,4 @@ final class UserProfileController extends AbstractController
'form' => $editForm->createView(),
]);
}
private function createPhonenumberEditForm(UserInterface $user): FormInterface
{
return $this->createForm(
UserPhonenumberType::class,
$user,
)
->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
}
}

View File

@ -14,6 +14,7 @@ namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@ -21,10 +22,10 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'chill_main_notification')]
#[ORM\Index(name: 'chill_main_notification_related_entity_idx', columns: ['relatedentityclass', 'relatedentityid'])]
#[ORM\Index(columns: ['relatedentityclass', 'relatedentityid'], name: 'chill_main_notification_related_entity_idx')]
class Notification implements TrackUpdateInterface
{
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false)]
#[ORM\Column(type: Types::TEXT, nullable: false)]
private string $accessKey;
private array $addedAddresses = [];
@ -36,12 +37,19 @@ class Notification implements TrackUpdateInterface
#[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
private Collection $addressees;
/**
* @var Collection<int, UserGroup>
*/
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
#[ORM\JoinTable(name: 'chill_main_notification_addressee_user_group')]
private Collection $addresseeUserGroups;
/**
* a list of destinee which will receive notifications.
*
* @var array|string[]
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
#[ORM\Column(type: Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
private array $addressesEmails = [];
/**
@ -60,21 +68,21 @@ class Notification implements TrackUpdateInterface
#[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
private Collection $comments;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private \DateTimeImmutable $date;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
#[ORM\Column(type: Types::TEXT)]
private string $message = '';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
#[ORM\Column(type: Types::STRING, length: 255)]
private string $relatedEntityClass = '';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[ORM\Column(type: Types::INTEGER)]
private int $relatedEntityId;
private array $removedAddresses = [];
@ -84,7 +92,7 @@ class Notification implements TrackUpdateInterface
private ?User $sender = null;
#[Assert\NotBlank(message: 'notification.Title must be defined')]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
#[ORM\Column(type: Types::TEXT, options: ['default' => ''])]
private string $title = '';
/**
@ -94,31 +102,46 @@ class Notification implements TrackUpdateInterface
#[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
private Collection $unreadBy;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private ?\DateTimeImmutable $updatedAt = null;
#[ORM\ManyToOne(targetEntity: User::class)]
private ?User $updatedBy = null;
#[ORM\Column(name: 'type', type: Types::STRING, nullable: true)]
private string $type = '';
public function __construct()
{
$this->addressees = new ArrayCollection();
$this->addresseeUserGroups = new ArrayCollection();
$this->unreadBy = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->setDate(new \DateTimeImmutable());
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
}
public function addAddressee(User $addressee): self
public function addAddressee(User|UserGroup $addressee): self
{
if (!$this->addressees->contains($addressee)) {
$this->addressees[] = $addressee;
$this->addedAddresses[] = $addressee;
if ($addressee instanceof User) {
if (!$this->addressees->contains($addressee)) {
$this->addressees->add($addressee);
$this->addedAddresses[] = $addressee;
}
return $this;
}
if (!$this->addresseeUserGroups->contains($addressee)) {
$this->addresseeUserGroups->add($addressee);
}
return $this;
}
/**
* @deprecated
*/
public function addAddressesEmail(string $email)
{
if (!\in_array($email, $this->addressesEmails, true)) {
@ -152,13 +175,23 @@ class Notification implements TrackUpdateInterface
#[Assert\Callback]
public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
{
if (0 === (\count($this->getAddressesEmails()) + \count($this->getAddressees()))) {
if (0 === (\count($this->getAddresseeUserGroups()) + \count($this->getAddressees()))) {
$context->buildViolation('notification.At least one addressee')
->atPath('addressees')
->addViolation();
}
}
public function getAddresseeUserGroups(): Collection
{
return $this->addresseeUserGroups;
}
public function setAddresseeUserGroups(Collection $addresseeUserGroups): void
{
$this->addresseeUserGroups = $addresseeUserGroups;
}
public function getAccessKey(): string
{
return $this->accessKey;
@ -182,6 +215,23 @@ class Notification implements TrackUpdateInterface
return $this->addressees;
}
public function getAllAddressees(): array
{
$allUsers = [];
foreach ($this->getAddressees() as $user) {
$allUsers[$user->getId()] = $user;
}
foreach ($this->getAddresseeUserGroups() as $userGroup) {
foreach ($userGroup->getUsers() as $user) {
$allUsers[$user->getId()] = $user;
}
}
return array_values($allUsers);
}
/**
* @return array|string[]
*/
@ -303,12 +353,18 @@ class Notification implements TrackUpdateInterface
$this->addressesOnLoad = null;
}
public function removeAddressee(User $addressee): self
public function removeAddressee(User|UserGroup $addressee): self
{
if ($this->addressees->removeElement($addressee)) {
$this->removedAddresses[] = $addressee;
if ($addressee instanceof User) {
if ($this->addressees->contains($addressee)) {
$this->addressees->removeElement($addressee);
return $this;
}
}
$this->addresseeUserGroups->removeElement($addressee);
return $this;
}
@ -378,7 +434,7 @@ class Notification implements TrackUpdateInterface
public function setUpdatedAt(\DateTimeInterface $datetime): self
{
$this->updatedAt = $datetime;
$this->updatedAt = \DateTimeImmutable::createFromInterface($datetime);
return $this;
}
@ -389,4 +445,16 @@ class Notification implements TrackUpdateInterface
return $this;
}
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
public function getType(): string
{
return $this->type;
}
}

View File

@ -34,6 +34,9 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
#[ORM\Table(name: 'users')]
class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInterface
{
public const NOTIF_FLAG_IMMEDIATE_EMAIL = 'immediate-email';
public const NOTIF_FLAG_DAILY_DIGEST = 'daily-digest';
#[ORM\Id]
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
@ -116,6 +119,12 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
#[PhonenumberConstraint]
private ?PhoneNumber $phonenumber = null;
/**
* @var array<string, list<string>>
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
private array $notificationFlags = [];
/**
* User constructor.
*/
@ -613,4 +622,57 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this;
}
/**
* Check if the current object is an instance of User.
*
* @return bool returns true if the current object is an instance of User, false otherwise
*/
public function isUser(): bool
{
return true;
}
public function getNotificationFlags(): array
{
return $this->notificationFlags;
}
public function setNotificationFlags(array $notificationFlags)
{
$this->notificationFlags = $notificationFlags;
}
public function getNotificationFlagData(string $flag): array
{
return $this->notificationFlags[$flag] ?? [];
}
public function setNotificationFlagData(string $flag, array $data): void
{
$this->notificationFlags[$flag] = $data;
}
public function isNotificationSendImmediately(string $type): bool
{
if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) {
return true;
}
return false;
}
public function isNotificationDailyDigest(string $type): bool
{
if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) {
return true;
}
return false;
}
public function getLocale(): string
{
return 'fr';
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form\DataMapper;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Form\DataMapperInterface;
final readonly class NotificationFlagDataMapper implements DataMapperInterface
{
public function __construct(private array $notificationFlagProviders) {}
public function mapDataToForms($viewData, $forms): void
{
if (null === $viewData) {
$viewData = [];
}
$formsArray = iterator_to_array($forms);
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
if (isset($formsArray[$flag])) {
$flagForm = $formsArray[$flag];
$immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true)
|| !array_key_exists($flag, $viewData);
$dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true);
if ($flagForm->has('immediate_email')) {
$flagForm->get('immediate_email')->setData($immediateEmailChecked);
}
if ($flagForm->has('daily_email')) {
$flagForm->get('daily_email')->setData($dailyEmailChecked);
}
}
}
}
public function mapFormsToData($forms, &$viewData): void
{
$formsArray = iterator_to_array($forms);
$viewData = [];
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
if (isset($formsArray[$flag])) {
$flagForm = $formsArray[$flag];
$viewData[$flag] = [];
if (true === $flagForm['immediate_email']->getData()) {
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
}
if (true === $flagForm['daily_email']->getData()) {
$viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST;
}
if ([] === $viewData[$flag]) {
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
}
}
}
}
}

View File

@ -12,17 +12,12 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
class NotificationType extends AbstractType
{
@ -33,29 +28,14 @@ class NotificationType extends AbstractType
'label' => 'Title',
'required' => true,
])
->add('addressees', PickUserDynamicType::class, [
->add('addressees', PickUserGroupOrUserDynamicType::class, [
'multiple' => true,
'required' => false,
'label' => 'notification.Pick user or user group',
'empty_data' => '[]',
'required' => true,
])
->add('message', ChillTextareaType::class, [
'required' => false,
])
->add('addressesEmails', ChillCollectionType::class, [
'label' => 'notification.dest by email',
'help' => 'notification.dest by email help',
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'entry_type' => EmailType::class,
'button_add_label' => 'notification.Add an email',
'button_remove_label' => 'notification.Remove an email',
'empty_collection_explain' => 'notification.Any email',
'entry_options' => [
'constraints' => [
new NotNull(), new NotBlank(), new Email(),
],
'label' => 'Email',
],
]);
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper;
use Chill\MainBundle\Notification\NotificationFlagManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class NotificationFlagsType extends AbstractType
{
private readonly array $notificationFlagProviders;
public function __construct(NotificationFlagManager $notificationFlagManager)
{
$this->notificationFlagProviders = $notificationFlagManager->getAllNotificationFlagProviders();
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
$builder->add($flag, FormType::class, [
'label' => $flagProvider->getLabel(),
'required' => false,
]);
$builder->get($flag)
->add('immediate_email', CheckboxType::class, [
'label' => false,
'required' => false,
'mapped' => false,
])
->add('daily_email', CheckboxType::class, [
'label' => false,
'required' => false,
'mapped' => false,
])
;
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\NotificationFlagsType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserProfileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('phonenumber', ChillPhoneNumberType::class, [
'required' => false,
])
->add('notificationFlags', NotificationFlagsType::class, [
'label' => false,
'mapped' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => \Chill\MainBundle\Entity\User::class,
]);
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\Email;
use Chill\MainBundle\Cron\CronJobInterface;
use Chill\MainBundle\Entity\CronJobExecution;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Messenger\MessageBusInterface;
readonly class DailyNotificationDigestCronjob implements CronJobInterface
{
public function __construct(
private ClockInterface $clock,
private Connection $connection,
private MessageBusInterface $messageBus,
private LoggerInterface $logger,
) {}
public function canRun(?CronJobExecution $cronJobExecution): bool
{
$now = $this->clock->now();
if (null !== $cronJobExecution && $now->sub(new \DateInterval('PT23H45M')) < $cronJobExecution->getLastStart()) {
return false;
}
// Run between 6 and 9 AM
return in_array((int) $now->format('H'), [6, 7, 8], true);
}
public function getKey(): string
{
return 'daily-notification-digest';
}
/**
* @throws \DateInvalidOperationException
* @throws Exception
*/
public function run(array $lastExecutionData): ?array
{
$now = $this->clock->now();
if (isset($lastExecutionData['last_execution'])) {
$lastExecution = \DateTimeImmutable::createFromFormat(
\DateTimeImmutable::ATOM,
$lastExecutionData['last_execution']
);
} else {
$lastExecution = $now->sub(new \DateInterval('P1D'));
}
// Get distinct users who received notifications since the last execution
$sql = <<<'SQL'
SELECT DISTINCT cmnau.user_id
FROM chill_main_notification cmn
JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id
WHERE cmn.date >= :lastExecution AND cmn.date <= :now
SQL;
$sqlStatement = $this->connection->prepare($sql);
$sqlStatement->bindValue('lastExecution', $lastExecution->format(\DateTimeInterface::RFC3339));
$sqlStatement->bindValue('now', $now->format(\DateTimeInterface::RFC3339));
$result = $sqlStatement->executeQuery();
$count = 0;
foreach ($result->fetchAllAssociative() as $row) {
$userId = (int) $row['user_id'];
$message = new ScheduleDailyNotificationDigestMessage(
$userId,
$lastExecution,
$now
);
$this->messageBus->dispatch($message);
++$count;
}
$this->logger->info('[DailyNotificationDigestCronjob] Dispatched daily digest messages', [
'user_count' => $count,
'last_execution' => $lastExecution->format('Y-m-d-H:i:s.u e'),
'current_time' => $now->format('Y-m-d-H:i:s.u e'),
]);
return [
'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
];
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
use Chill\MainBundle\Notification\Email\NotificationMailer;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Repository\UserRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
readonly class ScheduleDailyNotificationDigestHandler
{
public function __construct(
private NotificationRepository $notificationRepository,
private UserRepository $userRepository,
private NotificationMailer $notificationMailer,
private LoggerInterface $logger,
) {}
/**
* @throws TransportExceptionInterface
*/
public function __invoke(ScheduleDailyNotificationDigestMessage $message): void
{
$userId = $message->getUserId();
$lastExecutionDate = $message->getLastExecutionDateTime();
$currentDate = $message->getCurrentDateTime();
$user = $this->userRepository->find($userId);
if (null === $user) {
$this->logger->warning('[ScheduleDailyNotificationDigestHandler] User not found', [
'user_id' => $userId,
]);
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $userId));
}
// Get all notifications for this user between last execution and current date
$notifications = $this->notificationRepository->findNotificationsForUserBetweenDates(
$userId,
$lastExecutionDate,
$currentDate
);
// Filter out notifications that should be sent in a daily digest
$dailyNotifications = array_filter($notifications, fn ($notification) => $user->isNotificationDailyDigest($notification->getType()));
if ([] === $dailyNotifications) {
$this->logger->info('[ScheduleDailyNotificationDigestHandler] No daily notifications found for user', [
'user_id' => $userId,
]);
return;
}
$this->notificationMailer->sendDailyDigest($user, $dailyNotifications);
$this->logger->info('[ScheduleDailyNotificationDigestHandler] Sent daily digest', [
'user_id' => $userId,
'notification_count' => count($dailyNotifications),
]);
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
use Chill\MainBundle\Notification\Email\NotificationMailer;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Repository\UserRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
readonly class SendImmediateNotificationEmailHandler
{
public function __construct(
private NotificationRepository $notificationRepository,
private UserRepository $userRepository,
private NotificationMailer $notificationMailer,
private LoggerInterface $logger,
) {}
/**
* @throws TransportExceptionInterface
* @throws \Exception
*/
public function __invoke(SendImmediateNotificationEmailMessage $message): void
{
$notification = $this->notificationRepository->find($message->getNotificationId());
$addressee = $this->userRepository->find($message->getAddresseeId());
if (null === $notification) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [
'notification_id' => $message->getNotificationId(),
]);
throw new \InvalidArgumentException(sprintf('Notification with ID %s not found', $message->getNotificationId()));
}
if (null === $addressee) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [
'addressee_id' => $message->getAddresseeId(),
]);
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $message->getAddresseeId()));
}
try {
$this->notificationMailer->sendEmailToAddressee($notification, $addressee);
} catch (\Exception $e) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [
'notification_id' => $message->getNotificationId(),
'addressee_id' => $message->getAddresseeId(),
'stacktrace' => $e->getTraceAsString(),
]);
throw $e;
}
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\Email\NotificationEmailMessages;
readonly class ScheduleDailyNotificationDigestMessage
{
public function __construct(
private int $userId,
private \DateTimeInterface $lastExecutionDate,
private \DateTimeInterface $currentDate,
) {}
public function getUserId(): int
{
return $this->userId;
}
public function getLastExecutionDateTime(): \DateTimeInterface
{
return $this->lastExecutionDate;
}
public function getCurrentDateTime(): \DateTimeInterface
{
return $this->currentDate;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\Email\NotificationEmailMessages;
readonly class SendImmediateNotificationEmailMessage
{
public function __construct(
private int $notificationId,
private int $addresseeId,
) {}
public function getNotificationId(): int
{
return $this->notificationId;
}
public function getAddresseeId(): int
{
return $this->addresseeId;
}
}

View File

@ -13,22 +13,32 @@ namespace Chill\MainBundle\Notification\Email;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PostUpdateEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class NotificationMailer
readonly class NotificationMailer
{
public function __construct(private readonly MailerInterface $mailer, private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator) {}
public function __construct(
private MailerInterface $mailer,
private LoggerInterface $logger,
private MessageBusInterface $messageBus,
private TranslatorInterface $translator,
) {}
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
{
$dests = [$comment->getNotification()->getSender(), ...$comment->getNotification()->getAddressees()->toArray()];
$dests = [
$comment->getNotification()->getSender(),
...$comment->getNotification()->getAddressees()->toArray(),
];
$uniqueDests = [];
foreach ($dests as $dest) {
@ -69,55 +79,147 @@ class NotificationMailer
*/
public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void
{
$this->sendNotificationEmailsToAddresses($notification);
$this->sendNotificationEmailsToAddressees($notification);
$this->sendNotificationEmailsToAddressesEmails($notification);
}
public function postUpdateNotification(Notification $notification, PostUpdateEventArgs $eventArgs): void
private function sendNotificationEmailsToAddressees(Notification $notification): void
{
$this->sendNotificationEmailsToAddressesEmails($notification);
}
if ('' === $notification->getType()) {
$this->logger->warning('[NotificationMailer] Notification has no type, skipping email processing', [
'notification_id' => $notification->getId(),
]);
private function sendNotificationEmailsToAddresses(Notification $notification): void
{
foreach ($notification->getAddressees() as $addressee) {
return;
}
foreach ($notification->getAllAddressees() as $addressee) {
if (null === $addressee->getEmail()) {
continue;
}
if ($notification->isSystem()) {
$email = new Email();
$email
->text($notification->getMessage());
} else {
$email = new TemplatedEmail();
$email
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
->context([
'notification' => $notification,
'dest' => $addressee,
]);
}
$this->processNotificationForAddressee($notification, $addressee);
}
}
private function processNotificationForAddressee(Notification $notification, User $addressee): void
{
$notificationType = $notification->getType();
if ($addressee->isNotificationSendImmediately($notificationType)) {
$this->scheduleImmediateEmail($notification, $addressee);
}
}
private function scheduleImmediateEmail(Notification $notification, User $addressee): void
{
$message = new SendImmediateNotificationEmailMessage(
$notification->getId(),
$addressee->getId()
);
$this->messageBus->dispatch($message);
$this->logger->info('[NotificationMailer] Scheduled immediate email', [
'notification_id' => $notification->getId(),
'addressee_email' => $addressee->getEmail(),
]);
}
/**
* This method sends the email but is now called by the immediate notification email message handler.
*
* @throws TransportExceptionInterface
*/
public function sendEmailToAddressee(Notification $notification, User $addressee): void
{
if (null === $addressee->getEmail()) {
return;
}
if ($notification->isSystem()) {
$email = new Email();
$email->text($notification->getMessage());
} else {
$email = new TemplatedEmail();
$email
->subject($notification->getTitle())
->to($addressee->getEmail());
try {
$this->mailer->send($email);
} catch (TransportExceptionInterface $e) {
$this->logger->warning('[NotificationMailer] could not send an email notification', [
'to' => $addressee->getEmail(),
'error_message' => $e->getMessage(),
'error_trace' => $e->getTraceAsString(),
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
->context([
'notification' => $notification,
'dest' => $addressee,
]);
}
}
$email
->subject($notification->getTitle())
->to($addressee->getEmail());
try {
$this->mailer->send($email);
$this->logger->info('[NotificationMailer] Email sent successfully', [
'notification_id' => $notification->getId(),
'addressee_email' => $addressee->getEmail(),
]);
} catch (TransportExceptionInterface $e) {
$this->logger->warning('[NotificationMailer] Could not send an email notification', [
'to' => $addressee->getEmail(),
'notification_id' => $notification->getId(),
'error_message' => $e->getMessage(),
'error_trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
/**
* Send daily digest email with multiple notifications to a user.
*
* @throws TransportExceptionInterface
*/
public function sendDailyDigest(User $user, array $notifications): void
{
if (null === $user->getEmail() || [] === $notifications) {
return;
}
$email = new TemplatedEmail();
$email
->htmlTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig')
->context([
'user' => $user,
'notifications' => $notifications,
'notification_count' => count($notifications),
])
->subject($this->translator->trans('notification.Daily Notification Digest'))
->to($user->getEmail());
try {
$this->mailer->send($email);
$this->logger->info('[NotificationMailer] Daily digest email sent successfully', [
'user_email' => $user->getEmail(),
'notification_count' => count($notifications),
]);
} catch (TransportExceptionInterface $e) {
$this->logger->warning('[NotificationMailer] Could not send daily digest email', [
'to' => $user->getEmail(),
'notification_count' => count($notifications),
'error_message' => $e->getMessage(),
'error_trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
private function sendNotificationEmailsToAddressesEmails(Notification $notification): void
{
foreach ($notification->getAddressesEmailsAdded() as $emailAddress) {
foreach ($notification->getAddresseeUserGroups() as $userGroup) {
if (!$userGroup->hasEmail()) {
continue;
}
$emailAddress = $userGroup->getEmail();
$email = new TemplatedEmail();
$email
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\FlagProviders;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class NotificationByUserFlagProvider implements NotificationFlagProviderInterface
{
public const FLAG = 'notif-by-user';
public function getFlag(): string
{
return self::FLAG;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.by-user');
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\FlagProviders;
use Symfony\Contracts\Translation\TranslatableInterface;
interface NotificationFlagProviderInterface
{
public function getFlag(): string;
public function getLabel(): TranslatableInterface;
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\FlagProviders;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class WorkflowTransitionNotificationFlagProvider implements NotificationFlagProviderInterface
{
public const FLAG = 'workflow-trans-notif';
public function getFlag(): string
{
return self::FLAG;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.workflow-trans');
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification;
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
final readonly class NotificationFlagManager
{
/**
* @var array<NotificationFlagProviderInterface>
*/
private array $notificationFlagProviders;
public function __construct(
iterable $notificationFlagProviders,
) {
$this->notificationFlagProviders = iterator_to_array($notificationFlagProviders);
}
public function getAllNotificationFlagProviders(): array
{
return $this->notificationFlagProviders;
}
}

View File

@ -290,12 +290,19 @@ final class NotificationRepository implements ObjectRepository
return $qb;
}
private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder
private function queryByAddressee(User $addressee): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('n');
$qb
->where($qb->expr()->isMemberOf(':addressee', 'n.addressees'))
->leftJoin('n.addresseeUserGroups', 'aug')
->leftJoin('aug.users', 'ugu')
->where(
$qb->expr()->orX(
$qb->expr()->isMemberOf(':addressee', 'n.addressees'),
$qb->expr()->eq('ugu.id', ':addressee')
)
)
->setParameter('addressee', $addressee);
return $qb;
@ -393,4 +400,30 @@ final class NotificationRepository implements ObjectRepository
return $nq->getResult();
}
/**
* Find all notifications for a user that were created between two dates.
*
* @return array|Notification[]
*/
public function findNotificationsForUserBetweenDates(int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array
{
$rsm = new Query\ResultSetMappingBuilder($this->em);
$rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
$sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '.
'FROM chill_main_notification cmn '.
'JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id '.
'WHERE cmnau.user_id = :userId '.
'AND cmn.date >= :startDate '.
'AND cmn.date <= :endDate '.
'ORDER BY cmn.date DESC';
$nq = $this->em->createNativeQuery($sql, $rsm)
->setParameter('userId', $userId)
->setParameter('startDate', $startDate, Types::DATETIME_MUTABLE)
->setParameter('endDate', $endDate, Types::DATETIME_MUTABLE);
return $nq->getResult();
}
}

View File

@ -18,8 +18,9 @@
{%- endif -%}
{%- endblock form_label %}
{# this has been rewritten for chill #}
{% block form_label_class -%}
col-sm-4
{% if 'div_col_width' in label_attr|default({})|keys %}{% if label_attr['div_col_width'] is not same as false %}{{ label_attr['div_col_width'] }}{% endif %}{% else %}col-sm-4{% endif %}
{%- endblock form_label_class %}
{# Rows #}

View File

@ -69,41 +69,44 @@
{% endif %}
</li>
{% endif %}
{% if c.notification.addressees|length > 0 %}
{% if c.notification.addressees|length > 0 or c.notification.addresseeUserGroups|length > 0 %}
<li class="notification-to">
{% if c.notification_cc is defined %}
{% if c.notification_cc %}
<span class="item-key">
<abbr title="{{ 'notification.sent_cc' | trans }}">
{{ "notification.cc" | trans }} :
</abbr>
</span>
<abbr title="{{ 'notification.sent_cc' | trans }}">
{{ "notification.cc" | trans }} :
</abbr>
</span>
{% else %}
<span class="item-key">
<abbr title="{{ 'notification.sent_to' | trans }}">
{{ "notification.to" | trans }} :
</abbr>
</span>
<abbr title="{{ 'notification.sent_to' | trans }}">
{{ "notification.to" | trans }} :
</abbr>
</span>
{% endif %}
{% else %}
<span class="item-key">
<abbr title="{{ 'notification.sent_to' | trans }}">
{{ "notification.to" | trans }} :
</abbr>
</span>
<abbr title="{{ 'notification.sent_to' | trans }}">
{{ "notification.to" | trans }} :
</abbr>
</span>
{% endif %}
{% for a in c.notification.addressees %}
<span class="badge-user">
{{ a | chill_entity_render_string({'at_date': c.notification.date}) }}
</span>
{{ a | chill_entity_render_string({'at_date': c.notification.date}) }}
</span>
{% endfor %}
{% for a in c.notification.addressesEmails %}
<span
class="badge-user"
title="{{ 'notification.Email with access link'|trans|e('html_attr') }}"
>
{{ a }}
</span>
{{ a }}
</span>
{% endfor %}
{% for ug in c.notification.addresseeUserGroups %}
{{ ug|chill_entity_render_box }}
{% endfor %}
</li>
{% endif %}

View File

@ -21,8 +21,6 @@
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
{{ form_row(form.addressesEmails) }}
{% include handler.template(notification) with handler.templateData(notification) %}
<div class="mb-3 row">

View File

@ -0,0 +1,24 @@
{% apply markdown_to_html %}
# {{ 'notification.daily_digest.title'|trans }}
{{ 'notification.daily_digest.greeting'|trans({'%user%': user.label ?? user.email}) }},
{{ 'daily_notifications'|trans({'notification_count': notification_count}) }}
{% for notification in notifications %}
## {{ notification.title }}
{{ notification.message }}
{{ 'notification.daily_digest.view_notification'|trans }}
{{ absolute_url(path('chill_main_notification_show', {'_locale': user.locale, 'id': notification.id }, false)) }}
{% if not loop.last %}
---
{% endif %}
{% endfor %}
---
{{ 'notification.daily_digest.signature'|trans }}
{% endapply %}

View File

@ -20,7 +20,7 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{"My profile"|trans}}{% endblock %}
{% block title %}{{"user.profile.title"|trans}}{% endblock %}
{% block content %}
<div class="justify-content-center col-10">
@ -45,9 +45,35 @@
{{ form_start(form) }}
{{ form_row(form.phonenumber) }}
<h2 class="mb-4">{{ 'user.profile.notification_preferences'|trans }}</h2>
<table class="table table-striped align-middle">
<thead>
<tr>
<th>{{ 'notification.flags.type'|trans }}</th>
<th class="text-center">{{ 'notification.flags.preferences.immediate_email'|trans }}</th>
<th class="text-center">{{ 'notification.flags.preferences.daily_email'|trans }}</th>
</tr>
</thead>
<tbody class="table-hover table-group-divider">
{% for flag in form.notificationFlags %}
<tr>
<td class="col-sm-6">
{{ form_label(flag, null, {'label_attr': {'div_col_width': false}}) }}
</td>
<td class="text-center">
{{ form_widget(flag.immediate_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
</td>
<td class="text-center">
{{ form_widget(flag.daily_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<ul class="record_actions">
<li>
{{ form_widget(form.submit, { 'attr': { 'class': 'btn btn-save' } } ) }}
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
</li>
</ul>

View File

@ -9,7 +9,7 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code.
*/
namespace Entity;
namespace Chill\MainBundle\Tests\Entity;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
@ -49,8 +49,8 @@ final class NotificationTest extends KernelTestCase
$notification = new Notification();
$notification->addAddressee($user1 = new User());
$notification->addAddressee($user2 = new User());
$notification->getAddressees()->add($user3 = new User());
$notification->getAddressees()->add($user4 = new User());
$notification->addAddressee($user3 = new User());
$notification->addAddressee($user4 = new User());
$this->assertCount(4, $notification->getAddressees());
@ -85,6 +85,30 @@ final class NotificationTest extends KernelTestCase
$this->assertNotContains('other', $notification->getAddressesEmailsAdded());
}
public function testIsSendImmediately(): void
{
$notification = new Notification();
$notification->setType('test_notification_type');
$user = new User();
// no notification flags
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when no notification flags are set, by default immediate email');
// immediate-email preference
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_IMMEDIATE_EMAIL, User::NOTIF_FLAG_DAILY_DIGEST]]);
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when preferences contain immediate-email');
// daily-email preference
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_DAILY_DIGEST]]);
$this->assertFalse($user->isNotificationSendImmediately($notification->getType()), 'Should return false when preference is daily-email only');
$this->assertTrue($user->isNotificationDailyDigest($notification->getType()), 'Should return true when preference is daily-email');
// a different notification type
$notification->setType('other_notification_type');
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return false when notification type does not match any preference');
}
/**
* @dataProvider generateNotificationData
*/

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Tests\Notification\Email;
use Chill\MainBundle\Notification\Email\DailyNotificationDigestCronjob;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* Run functional test on the cronjob.
*
* @internal
*
* @coversNothing
*/
class DailyNotificationDigestCronJobFunctionalTest extends KernelTestCase
{
private DailyNotificationDigestCronjob $dailyNotificationDigestCronjob;
protected function setUp(): void
{
self::bootKernel();
$this->dailyNotificationDigestCronjob = self::getContainer()->get(DailyNotificationDigestCronjob::class);
}
public function testRunWithNullPreviousExecutionData(): void
{
$actual = $this->dailyNotificationDigestCronjob->run([]);
self::assertArrayHasKey('last_execution', $actual);
self::assertInstanceOf(
\DateTimeImmutable::class,
\DateTimeImmutable::createFromFormat('Y-m-d-H:i:s.u e', $actual['last_execution']),
'test that the string can be converted to a date'
);
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Tests\Notification\Email;
use Chill\MainBundle\Notification\Email\DailyNotificationDigestCronjob;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* @internal
*
* @coversNothing
*/
class DailyNotificationDigestCronJobTest extends TestCase
{
private ClockInterface $clock;
private Connection $connection;
private MessageBusInterface $messageBus;
private LoggerInterface $logger;
private DailyNotificationDigestCronjob $cronjob;
protected function setUp(): void
{
$this->clock = $this->createMock(ClockInterface::class);
$this->connection = $this->createMock(Connection::class);
$this->messageBus = $this->createMock(MessageBusInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->cronjob = new DailyNotificationDigestCronjob(
$this->clock,
$this->connection,
$this->messageBus,
$this->logger
);
}
public function testGetKey(): void
{
$this->assertEquals('daily-notification-digest', $this->cronjob->getKey());
}
/**
* @dataProvider canRunTimeDataProvider
*/
public function testCanRunWithNullCronJobExecution(int $hour, bool $expected): void
{
$now = new \DateTimeImmutable("2024-01-01 {$hour}:00:00");
$this->clock->expects($this->once())
->method('now')
->willReturn($now);
$result = $this->cronjob->canRun(null);
$this->assertEquals($expected, $result);
}
public static function canRunTimeDataProvider(): array
{
return [
'hour 5 - should not run' => [5, false],
'hour 6 - should run' => [6, true],
'hour 7 - should run' => [7, true],
'hour 8 - should run' => [8, true],
'hour 9 - should not run' => [9, false],
'hour 10 - should not run' => [10, false],
'hour 23 - should not run' => [23, false],
];
}
}

View File

@ -17,13 +17,18 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\Email\NotificationMailer;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PostPersistEventArgs;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Translation\Translator;
use Symfony\Contracts\Translation\TranslatorInterface;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
/**
* @internal
@ -112,13 +117,149 @@ class NotificationMailerTest extends TestCase
$mailer->postPersistComment($comment, new PostPersistEventArgs($comment, $objectManager->reveal()));
}
/**
* @throws \ReflectionException
* @throws Exception
*/
public function testProcessNotificationForAddresseeWithImmediateEmailPreference(): void
{
// Create a real notification entity
$notification = new Notification();
$notification->setType('test_notification_type');
// Use reflection to set the ID since it's normally generated by the database
$reflectionNotification = new \ReflectionClass(Notification::class);
$idProperty = $reflectionNotification->getProperty('id');
$idProperty->setAccessible(true);
$idProperty->setValue($notification, 123);
// Create a real user entity
$user = new User();
$user->setEmail('user@example.com');
// Use reflection to set the ID since it's normally generated by the database
$reflectionUser = new \ReflectionClass(User::class);
$idProperty = $reflectionUser->getProperty('id');
$idProperty->setAccessible(true);
$idProperty->setValue($user, 456);
// Set notification flags for the user
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_IMMEDIATE_EMAIL]]);
$messageBus = $this->createMock(MessageBusInterface::class);
$messageBus->expects($this->once())
->method('dispatch')
->with($this->callback(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId()
&& 456 === $message->getAddresseeId()))
->willReturn(new Envelope(new \stdClass()));
$mailer = $this->buildNotificationMailer(null, $messageBus);
// Call the method that processes notifications
$reflection = new \ReflectionClass(NotificationMailer::class);
$method = $reflection->getMethod('processNotificationForAddressee');
$method->setAccessible(true);
$method->invoke($mailer, $notification, $user);
}
public function testSendDailyDigest(): void
{
// Create a user
$user = new User();
$user->setEmail('user@example.com');
// Create some notifications
$notification = $this->prophesize(Notification::class);
$notification->getTitle()->willReturn('Notification 1');
$notification->getMessage()->willReturn('Message 1');
$notification->getId()->willReturn(123);
$notification2 = $this->prophesize(Notification::class);
$notification2->getTitle()->willReturn('Notification 2');
$notification2->getMessage()->willReturn('Message 2');
$notification2->getId()->willReturn(456);
$notifications = [$notification, $notification2];
// Mock the mailer to verify that an email is sent with the correct parameters
$mailer = $this->prophesize(MailerInterface::class);
$mailer->send(Argument::that(function ($email) use ($user) {
// Verify that the email is sent to the correct user
foreach ($email->getTo() as $address) {
if ($user->getEmail() === $address->getAddress()) {
return true;
}
}
return false;
}))->shouldBeCalledOnce();
// Create a translator that returns a fixed string for the subject
$translator = $this->prophesize(TranslatorInterface::class);
$translator->trans('notification.Daily Notification Digest')->willReturn('Daily Digest');
// Create the notification mailer with the mocked mailer and translator
$notificationMailer = $this->buildNotificationMailer($mailer->reveal(), null, $translator->reveal());
// Call the sendDailyDigest method
$notificationMailer->sendDailyDigest($user, $notifications);
}
public function testSendDailyDigestWithNoNotifications(): void
{
// Create a user
$user = new User();
$user->setEmail('user@example.com');
// Empty notifications array
$notifications = [];
// Mock the mailer to verify that no email is sent
$mailer = $this->prophesize(MailerInterface::class);
$mailer->send(Argument::any())->shouldNotBeCalled();
// Create the notification mailer with the mocked mailer
$notificationMailer = $this->buildNotificationMailer($mailer->reveal());
// Call the sendDailyDigest method
$notificationMailer->sendDailyDigest($user, $notifications);
}
public function testSendDailyDigestWithUserHavingNoEmail(): void
{
// Create a user with no email
$user = new User();
$user->setEmail(null);
// Create some notifications
$notification = $this->prophesize(Notification::class);
$notification->getTitle()->willReturn('Notification 1');
$notification->getMessage()->willReturn('Message 1');
$notification->getId()->willReturn(123);
$notifications = [$notification];
// Mock the mailer to verify that no email is sent
$mailer = $this->prophesize(MailerInterface::class);
$mailer->send(Argument::any())->shouldNotBeCalled();
// Create the notification mailer with the mocked mailer
$notificationMailer = $this->buildNotificationMailer($mailer->reveal());
// Call the sendDailyDigest method
$notificationMailer->sendDailyDigest($user, $notifications);
}
private function buildNotificationMailer(
?MailerInterface $mailer = null,
?MessageBusInterface $messageBus = null,
?TranslatorInterface $translator = null,
): NotificationMailer {
return new NotificationMailer(
$mailer,
$mailer ?? $this->prophesize(MailerInterface::class)->reveal(),
new NullLogger(),
new Translator('fr')
$messageBus ?? $this->prophesize(MessageBusInterface::class)->reveal(),
$translator ?? new Translator('fr')
);
}
}

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Notification\FlagProviders\WorkflowTransitionNotificationFlagProvider;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Doctrine\ORM\EntityManagerInterface;
@ -125,7 +126,8 @@ class NotificationOnTransition implements EventSubscriberInterface
->setRelatedEntityClass(EntityWorkflow::class)
->setTitle($this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_title.fr.txt.twig', $context))
->setMessage($this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_content.fr.txt.twig', $context))
->addAddressee($subscriber);
->addAddressee($subscriber)
->setType(WorkflowTransitionNotificationFlagProvider::FLAG);
$this->entityManager->persist($notification);
}
}

View File

@ -139,6 +139,11 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper:
autowire: true
autoconfigure: true
Chill\MainBundle\Form\UserProfileType: ~
Chill\MainBundle\Form\AbsenceType: ~
Chill\MainBundle\Form\DataMapper\RegroupmentDataMapper: ~
Chill\MainBundle\Form\RegroupmentType: ~

View File

@ -12,6 +12,10 @@ services:
arguments:
$routeParameters: '%chill_main.notifications%'
Chill\MainBundle\Notification\NotificationFlagManager:
arguments:
$notificationFlagProviders: !tagged_iterator chill_main.notification_flag_provider
Chill\MainBundle\Notification\NotificationHandlerManager:
arguments:
$handlers: !tagged_iterator chill_main.notification_handler
@ -55,14 +59,6 @@ services:
lazy: true
method: 'postPersistNotification'
-
name: 'doctrine.orm.entity_listener'
event: 'postUpdate'
entity: 'Chill\MainBundle\Entity\Notification'
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
lazy: true
method: 'postUpdateNotification'
-
name: 'doctrine.orm.entity_listener'
event: 'postPersist'

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250610102953 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add notification flags property to User';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE users ADD notificationFlags JSONB DEFAULT '[]' NOT NULL
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE users DROP notificationFlags
SQL);
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250618115938 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add type property to notifications';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE chill_main_notification ADD type VARCHAR(255) NOT NULL DEFAULT 'default_notification_type'
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE chill_main_notification DROP type
SQL);
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250623120824 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add addressee user groups to notifications';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE chill_main_notification_addressee_user_group (notification_id INT NOT NULL, usergroup_id INT NOT NULL, PRIMARY KEY(notification_id, usergroup_id))
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_ECF81C07EF1A9D84 ON chill_main_notification_addressee_user_group (notification_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_ECF81C07D2112630 ON chill_main_notification_addressee_user_group (usergroup_id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_main_notification_addressee_user_group ADD CONSTRAINT FK_ECF81C07EF1A9D84 FOREIGN KEY (notification_id) REFERENCES chill_main_notification (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_main_notification_addressee_user_group ADD CONSTRAINT FK_ECF81C07D2112630 FOREIGN KEY (usergroup_id) REFERENCES chill_main_user_group (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE chill_main_notification_addressee_user_group DROP CONSTRAINT FK_ECF81C07EF1A9D84
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_main_notification_addressee_user_group DROP CONSTRAINT FK_ECF81C07D2112630
SQL);
$this->addSql(<<<'SQL'
DROP TABLE chill_main_notification_addressee_user_group
SQL);
}
}

View File

@ -49,6 +49,12 @@ notification:
other {# commentaires}
}
daily_notifications: >-
{notification_count, plural,
=1 {Voici votre notification du jour :}
other {Voici vos # notifications du jour :}
}
workflow:
My workflows with counter: >-
{wc, plural,

View File

@ -52,9 +52,10 @@ user:
current_user: Utilisateur courant
profile:
title: Mon profil
Phonenumber successfully updated!: Numéro de téléphone mis à jour!
Profile successfully updated!: Votre profil a été mis à jour!
no job: Pas de métier assigné
no scope: Pas de cercle assigné
notification_preferences: Préférences pour mes notifications
user_group:
inactive: Inactif
@ -674,6 +675,7 @@ Subscribe all steps: Recevoir une notification à chaque étape
CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION: Appliquer les transitions sur tous les workflows
notification:
Daily Notification Digest: Résumé des notifications quotidiennes
Notification: Notification
Notifications: Notifications
My own notifications: Mes notifications
@ -712,13 +714,36 @@ notification:
dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Un compte utilisateur sera toujours nécessaire.
Remove an email: Supprimer l'adresse email
Email with access link: Adresse email ayant reçu un lien d'accès
Pick user or user group: Selectionner un utilisateur / groupe d'utilisateurs
mark_as_read: Marquer comme lu
mark_as_unread: Marquer comme non-lu
flags:
type: Type de notification
by-user: Lorsqu'un utilisateur vous envoie une notification personnelle
referrer-acc-course: Lorsqu'un autre utilisateur vous désigne comme référent d'un parcours
person-address-move: Lorsqu'un autre utilisateur enregistre le déménagement d'un usager concerné par un parcours dont vous êtes le référent.
person: Notification sur un usager
workflow-trans: Lorsqu'un autre utilisateur applique une transition à un workflow.
none selected message: Si vous ne sélectionnez aucune option, vous ne recevrez pas d'email concernant ce type de notification.
preferences:
column_title: Préférences
immediate_email: Email immédiat
daily_email: Récapitulatif quotidien
no_email: Ne pas recevoir un email
daily_digest:
title: "Résumé quotidien des notifications"
greeting: "Bonjour %user%"
intro: "Vous avez reçu %notification_count% nouvelle(s) notification(s)."
view_notification: "Vous pouvez visualiser la notification et y répondre ici:"
signature: "Le logiciel Chill"
CHILL_MAIN_COMPOSE_EXPORT: Exécuter des exports et les sauvegarder
CHILL_MAIN_GENERATE_SAVED_EXPORT: Exécuter et modifier des exports préalablement sauvegardés
export:
role:
export_role: Exports

View File

@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\NotificationPersisterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
use Chill\PersonBundle\Notification\FlagProviders\PersonAddressMoveNotificationFlagProvider;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
@ -65,7 +66,8 @@ class PersonAddressMoveEventSubscriber implements EventSubscriberInterface
->setMessage($this->engine->render('@ChillPerson/AccompanyingPeriod/notification_location_user_on_period_has_moved.fr.txt.twig', [
'oldPersonLocation' => $person,
'period' => $period,
]));
]))
->setType(PersonAddressMoveNotificationFlagProvider::FLAG);
$this->notificationPersister->persist($notification);
}

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\NotificationPersisterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Notification\FlagProviders\DesignatedReferrerNotificationFlagProvider;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
@ -73,7 +74,8 @@ class UserRefEventSubscriber implements EventSubscriberInterface
'accompanyingCourse' => $period,
]
))
->addAddressee($period->getUser());
->addAddressee($period->getUser())
->setType(DesignatedReferrerNotificationFlagProvider::FLAG);
$this->notificationPersister->persist($notification);
}

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle;
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
use Chill\PersonBundle\Actions\Remove\PersonMoveSqlHandlerInterface;
use Chill\PersonBundle\DependencyInjection\CompilerPass\AccompanyingPeriodTimelineCompilerPass;
use Chill\PersonBundle\Export\Helper\CustomizeListPersonHelperInterface;
@ -35,5 +36,7 @@ class ChillPersonBundle extends Bundle
->addTag('chill_person.person_move_handler');
$container->registerForAutoconfiguration(CustomizeListPersonHelperInterface::class)
->addTag('chill_person.list_person_customizer');
$container->registerForAutoconfiguration(NotificationFlagProviderInterface::class)
->addTag('chill_main.notification_flag_provider');
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Notification\FlagProviders;
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class DesignatedReferrerNotificationFlagProvider implements NotificationFlagProviderInterface
{
public const FLAG = 'referrer-acc-course-notif';
public function getFlag(): string
{
return self::FLAG;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.referrer-acc-course');
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Notification\FlagProviders;
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class PersonAddressMoveNotificationFlagProvider implements NotificationFlagProviderInterface
{
public const FLAG = 'person-move-notif';
public function getFlag(): string
{
return self::FLAG;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.person-address-move');
}
}

View File

@ -1,4 +1,8 @@
services:
_defaults:
autowire: true
autoconfigure: true
Chill\PersonBundle\Notification\AccompanyingPeriodNotificationHandler:
autowire: true
autoconfigure: true
@ -8,3 +12,5 @@ services:
Chill\PersonBundle\Notification\AccompanyingPeriodWorkEvaluationDocumentNotificationHandler:
autowire: true
autoconfigure: true
Chill\PersonBundle\Notification\FlagProviders\DesignatedReferrerNotificationFlagProvider: ~
Chill\PersonBundle\Notification\FlagProviders\PersonAddressMoveNotificationFlagProvider: ~