mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-07-29 20:17:44 +00:00
Resolve "Notification: envoi à des groupes utilisateurs"
This commit is contained in:
parent
5bdb2df929
commit
ab8da4ab7a
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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')]);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
41
src/Bundle/ChillMainBundle/Form/UserProfileType.php
Normal file
41
src/Bundle/ChillMainBundle/Form/UserProfileType.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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')
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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 #}
|
||||
|
@ -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 %}
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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'
|
||||
);
|
||||
}
|
||||
}
|
@ -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],
|
||||
];
|
||||
}
|
||||
}
|
@ -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')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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: ~
|
||||
|
@ -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'
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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: ~
|
||||
|
Loading…
x
Reference in New Issue
Block a user