Compare commits

..

12 Commits

Author SHA1 Message Date
27d344c97d Release v3.12.0 2025-06-30 11:00:42 +02:00
088e5692e2 Merge branch 'improve_person_resource_form' into 'master'
Improve admin templates for event admin entities + activity reason (category)...

See merge request Chill-Projet/chill-bundles!838
2025-06-30 08:44:24 +00:00
298044bc82 Improve admin templates for event admin entities + activity reason (category)... 2025-06-30 08:44:24 +00:00
ee4e223043 Merge branch '393-fix-dump-only-document-generator' into 'master'
Send data dumps as email attachments instead of links, update translations,...

Closes #393

See merge request Chill-Projet/chill-bundles!843
2025-06-30 08:41:09 +00:00
c53377ce8d Merge branch 'workflow-do-not-remove-workflow-canceled-automatically' into 'master'
Remove unnecessary workflow deletion logic when in the initial position

See merge request Chill-Projet/chill-bundles!844
2025-06-30 08:40:46 +00:00
0b580658de Remove unnecessary workflow deletion logic when in the initial position 2025-06-26 14:38:12 +02:00
786c60a50d Send data dumps as email attachments instead of links, update translations, and add unit tests for the handler. 2025-06-26 12:21:19 +02:00
456f00566d update juni guidelines 2025-06-26 12:19:49 +02:00
a38116cca4 fix cs 2025-06-20 17:31:13 +02:00
nobohan
9158e33854 #392 php cs-fixer 2025-06-19 21:29:43 +02:00
nobohan
af74f7860b Fixed nullable content for NewsItem in setContent signature #392 2025-06-19 17:53:33 +02:00
bdf1cf71ba Fix argument usage in localizeString method for UserRenderBoxBadge component
Adjusted `localizeString` method to accept a `label` parameter, ensuring proper localization of `user_job.label` and `main_scope.name`. Also made minor syntax adjustments to improve readability.
2025-06-17 10:55:30 +02:00
146 changed files with 2663 additions and 3858 deletions

View File

@@ -1,6 +0,0 @@
kind: DX
body: Remove dead code for wopi-link module
time: 2025-04-30T14:45:50.406111606+02:00
custom:
Issue: "352"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: DX
body: Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
time: 2025-05-28T16:58:13.226870341+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,7 +0,0 @@
kind: Feature
body: Add the document file name to the document title when a user upload a document,
unless there is already a document title.
time: 2025-04-24T14:22:11.800975422+02:00
custom:
Issue: "377"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Add desactivation date for social action and issue csv export
time: 2025-05-20T09:56:28.108941934+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Add Emoji and Fullscreen feature to ckeditor configuration
time: 2025-05-23T13:33:41.645095128+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Create editor which allow us to toggle between rich and simple text editor
time: 2025-05-23T13:34:34.56795603+02:00
custom:
Issue: "321"
SchemaChange: No schema change

View File

@@ -1,7 +0,0 @@
kind: Fixed
body: trying to prevent bug of typeerror in doc-history + improved display of document
history
time: 2025-04-24T13:39:43.878468232+02:00
custom:
Issue: "376"
SchemaChange: No schema change

View File

@@ -1,7 +0,0 @@
kind: Fixed
body: Display previous participation in acc course work even if the person has left
the acc course
time: 2025-04-24T16:37:46.970203594+02:00
custom:
Issue: "381"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Fix display of text in calendar events
time: 2025-05-05T10:27:15.461493066+02:00
custom:
Issue: "372"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Add missing translation for user_group.no_user_groups
time: 2025-05-14T14:53:39.53927329+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: UX
body: Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
time: 2025-04-23T17:26:24.45777387+02:00
custom:
Issue: "374"
SchemaChange: No schema change

22
.changes/v3.12.0.md Normal file
View File

@@ -0,0 +1,22 @@
## v3.12.0 - 2025-06-30
### Feature
* ([#377](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/377)) Add the document file name to the document title when a user upload a document, unless there is already a document title.
* Add desactivation date for social action and issue csv export
* Add Emoji and Fullscreen feature to ckeditor configuration
* ([#321](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/321)) Create editor which allow us to toggle between rich and simple text editor
* Do not remove workflow which are automatically canceled after staling for more than 30 days
### Fixed
* ([#376](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/376)) trying to prevent bug of typeerror in doc-history + improved display of document history
* ([#381](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/381)) Display previous participation in acc course work even if the person has left the acc course
* ([#372](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/372)) Fix display of text in calendar events
* Add missing translation for user_group.no_user_groups
* Fix admin entity edit actions for event admin entities and activity reason (category) entities
* ([#392](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/392)) Allow null and cast as string to setContent method for NewsItem
* ([#393](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/393)) Doc Generation: the "dump only" method send the document as an email attachment.
### DX
* ([#352](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/352)) Remove dead code for wopi-link module
* Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
### UX
* ([#374](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/374)) Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
* Improve labeling of fields in person resource creation form

View File

@@ -22,7 +22,7 @@ Chill is a comprehensive web application built as a set of Symfony bundles. It i
- **Backend**: PHP 8.3+, Symfony 5.4
- **Frontend**: JavaScript/TypeScript, Vue.js 3, Bootstrap 5
- **Build Tools**: Webpack Encore, Yarn
- **Database**: PostgreSQL with materialized views
- **Database**: PostgreSQL with materialized views. We do not support other databases.
- **Other Services**: Redis, AMQP (RabbitMQ), SMTP
## Project Structure
@@ -149,6 +149,42 @@ Key configuration files:
- `package.json`: JavaScript dependencies and scripts
- `.env`: Default environment variables. Must usually not be updated: use `.env.local` instead.
### Database migrations
Each time a doctrine entity is created, we generate migration to adapt the database.
The migration are created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, do not forget to quote the `\` (`\` must become `\\` in your command).
Each bundle has his own namespace for migration (always ask me to confirm that command, with a list of updated / created entities so that I can confirm you that it is ok):
- `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`;
- `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`;
- `Chill\Bundle\CustomFieldsBundle` writes migrations to `Chill\Migrations\CustomFields`;
- `Chill\Bundle\DocGeneratorBundle` writes migrations to `Chill\Migrations\DocGenerator`;
- `Chill\Bundle\DocStoreBundle` writes migrations to `Chill\Migrations\DocStore`;
- `Chill\Bundle\EventBundle` writes migrations to `Chill\Migrations\Event`;
- `Chill\Bundle\CalendarBundle` writes migrations to `Chill\Migrations\Calendar`;
- `Chill\Bundle\FamilyMembersBundle` writes migrations to `Chill\Migrations\FamilyMembers`;
- `Chill\Bundle\FranceTravailApiBundle` writes migrations to `Chill\Migrations\FranceTravailApi`;
- `Chill\Bundle\JobBundle` writes migrations to `Chill\Migrations\Job`;
- `Chill\Bundle\MainBundle` writes migrations to `Chill\Migrations\Main`;
- `Chill\Bundle\PersonBundle` writes migrations to `Chill\Migrations\Person`;
- `Chill\Bundle\ReportBundle` writes migrations to `Chill\Migrations\Report`;
- `Chill\Bundle\TaskBundle` writes migrations to `Chill\Migrations\Task`;
- `Chill\Bundle\ThirdPartyBundle` writes migrations to `Chill\Migrations\ThirdParty`;
- `Chill\Bundle\TicketBundle` writes migrations to `Chill\Migrations\Ticket`;
- `Chill\Bundle\WopiBundle` writes migrations to `Chill\Migrations\Wopi`;
Once created the, comment's classes should be removed and a description of the changes made to the entities should be added to the migrations, using the `getDescription` method. The migration should not be cleaned by any artificial intelligence, as modifying this migration is error prone.
### Guidelines related to code structure and requirements
#### Usage of clock
When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of
`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.
### 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.
@@ -218,7 +254,7 @@ class TicketTest extends TestCase
#### Test Database
For tests that require a database, the project uses an in-memory SQLite database by default. You can configure a different database for testing in the `.env.test` file.
For tests that require a database, the project uses postgresql database filled by fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file.
### Code Quality Tools

View File

@@ -6,6 +6,49 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v3.12.0 - 2025-06-30
### Feature
* ([#377](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/377)) Add the document file name to the document title when a user upload a document, unless there is already a document title.
* Add desactivation date for social action and issue csv export
* Add Emoji and Fullscreen feature to ckeditor configuration
* ([#321](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/321)) Create editor which allow us to toggle between rich and simple text editor
* Do not remove workflow which are automatically canceled after staling for more than 30 days
### Fixed
* ([#376](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/376)) trying to prevent bug of typeerror in doc-history + improved display of document history
* ([#381](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/381)) Display previous participation in acc course work even if the person has left the acc course
* ([#372](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/372)) Fix display of text in calendar events
* Add missing translation for user_group.no_user_groups
* Fix admin entity edit actions for event admin entities and activity reason (category) entities
* ([#392](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/392)) Allow null and cast as string to setContent method for NewsItem
* ([#393](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/393)) Doc Generation: the "dump only" method send the document as an email attachment.
### DX
* ([#352](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/352)) Remove dead code for wopi-link module
* Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
### UX
* ([#374](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/374)) Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
* Improve labeling of fields in person resource creation form
## v3.11.0 - 2025-04-17
### Feature
* ([#365](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/365)) Add counters of actions and activities, with 2 boxes to (1) show the number of active actions on total actions and (2) show the number of activities in a accompanying period, and pills in menus for showing the number of active actions and the number of activities.
* ([#364](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/364)) Added a second phone number "telephone2" to the thirdParty entity. Adapted twig templates and vuejs apps to handle this phone number
**Schema Change**: Add columns or tables
* Signature: add a button to go directly to the signature zone, even if there is only one
### Fixed
* Fixed wrong translations in the on-the-fly for creation of thirdParty
* Fixed update of phone number in on-the-fly edition of thirdParty
* Fixed closing of modal when editing thirdParty in accompanying course works
* Shorten the delay between two execution of AccompanyingPeriodStepChangeCronjob, to ensure at least one execution in a day
* ([#102](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/102)) Fix display of title in document list
* When cleaning the old stored object versions, do not throw an error if the stored object is not found on disk
* Add consistent log prefix and key to logs when stale workflows are automatically canceled
* ([#380](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/380)) Remove the "not null" validation constraint on recently added properties on HouseholdComposition
### DX
* Add new chill-col style for displaying title and aside in a flex table
## v3.10.3 - 2025-03-18
### DX
* Eslint fixes

View File

@@ -12,7 +12,3 @@ framework:
adapter: cache.adapter.redis
public: false
default_lifetime: 300
cache.daily_notifications:
adapter: cache.adapter.redis
public: true
default_lifetime: 90000 # 25 hours

View File

@@ -45,27 +45,6 @@ framework:
auto_setup: false
immediate_email:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%/priority'
options:
queue_name: immediate_notifications
exchange:
name: notifications
type: direct
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
daily_email:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
queue_name: daily_notifications
exchange:
name: notifications
type: direct
# No automatic consumption - handled by cron job
routing:
# routes added by chill-bundles recipes
'Chill\CalendarBundle\Messenger\Message\CalendarRangeMessage': async
@@ -82,9 +61,6 @@ framework:
'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority
'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async
'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
'Chill\MainBundle\Notification\Email\SendImmediateNotificationEmailMessage': immediate_email
'Chill\MainBundle\Notification\Email\ScheduleDailyNotificationEmailMessage': daily_email
'Chill\MainBundle\Notification\Email\SendDailyDigestMessage': daily_email
# end of routes added by chill-bundles recipes
# Route your messages to the transports
# 'App\Message\YourMessage': async

View File

@@ -48,28 +48,6 @@ class ActivityReasonCategoryController extends AbstractController
]);
}
/**
* Displays a form to edit an existing ActivityReasonCategory entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/edit', name: 'chill_activity_activityreasoncategory_edit')]
public function editAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReasonCategory::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find ActivityReasonCategory entity.');
}
$editForm = $this->createEditForm($entity);
return $this->render('@ChillActivity/ActivityReasonCategory/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
]);
}
/**
* Lists all ActivityReasonCategory entities.
*/
@@ -100,29 +78,10 @@ class ActivityReasonCategoryController extends AbstractController
]);
}
/**
* Finds and displays a ActivityReasonCategory entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/show', name: 'chill_activity_activityreasoncategory_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReasonCategory::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find ActivityReasonCategory entity.');
}
return $this->render('@ChillActivity/ActivityReasonCategory/show.html.twig', [
'entity' => $entity,
]);
}
/**
* Edits an existing ActivityReasonCategory entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/update', name: 'chill_activity_activityreasoncategory_update', methods: ['POST', 'PUT'])]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/update', name: 'chill_activity_activityreasoncategory_update')]
public function updateAction(Request $request, mixed $id)
{
$em = $this->managerRegistry->getManager();
@@ -139,7 +98,7 @@ class ActivityReasonCategoryController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) {
$em->flush();
return $this->redirectToRoute('chill_activity_activityreasoncategory_edit', ['id' => $id]);
return $this->redirectToRoute('chill_activity_activityreasoncategory', ['id' => $id]);
}
return $this->render('@ChillActivity/ActivityReasonCategory/edit.html.twig', [
@@ -178,7 +137,7 @@ class ActivityReasonCategoryController extends AbstractController
{
$form = $this->createForm(ActivityReasonCategoryType::class, $entity, [
'action' => $this->generateUrl('chill_activity_activityreasoncategory_update', ['id' => $entity->getId()]),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -50,28 +50,6 @@ class ActivityReasonController extends AbstractController
]);
}
/**
* Displays a form to edit an existing ActivityReason entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/edit', name: 'chill_activity_activityreason_edit')]
public function editAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReason::class)->find($id);
if (null === $entity) {
throw new NotFoundHttpException('Unable to find ActivityReason entity.');
}
$editForm = $this->createEditForm($entity);
return $this->render('@ChillActivity/ActivityReason/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
]);
}
/**
* Lists all ActivityReason entities.
*/
@@ -102,29 +80,10 @@ class ActivityReasonController extends AbstractController
]);
}
/**
* Finds and displays a ActivityReason entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/show', name: 'chill_activity_activityreason_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReason::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find ActivityReason entity.');
}
return $this->render('@ChillActivity/ActivityReason/show.html.twig', [
'entity' => $entity,
]);
}
/**
* Edits an existing ActivityReason entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/update', name: 'chill_activity_activityreason_update', methods: ['POST', 'PUT'])]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/update', name: 'chill_activity_activityreason_update')]
public function updateAction(Request $request, mixed $id)
{
$em = $this->managerRegistry->getManager();
@@ -180,7 +139,7 @@ class ActivityReasonController extends AbstractController
{
$form = $this->createForm(ActivityReasonType::class, $entity, [
'action' => $this->generateUrl('chill_activity_activityreason_update', ['id' => $entity->getId()]),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -3,7 +3,7 @@
{% block admin_content %}
<h1>{{ 'ActivityReason list'|trans }}</h1>
<table class="records_list">
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
@@ -29,10 +29,7 @@
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activityreason_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_activity_activityreason_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
<a href="{{ path('chill_activity_activityreason_update', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
</ul>
</td>

View File

@@ -3,7 +3,7 @@
{% block admin_content %}
<h1>{{ 'ActivityReasonCategory list'|trans }}</h1>
<table class="records_list">
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
@@ -23,10 +23,7 @@
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activityreasoncategory_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_activity_activityreasoncategory_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
<a href="{{ path('chill_activity_activityreasoncategory_update', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
</ul>
</td>

View File

@@ -22,6 +22,52 @@ use Symfony\Component\Security\Core\Role\Role;
*/
final class ActivityControllerTest extends WebTestCase
{
/**
* @dataProvider getSecuredPagesUnauthenticated
*/
public function testAccessIsDeniedForUnauthenticated(mixed $url)
{
$client = $this->createClient();
$client->request('GET', $url);
$this->assertEquals(302, $client->getResponse()->getStatusCode());
$this->assertTrue(
$client->getResponse()->isRedirect('http://localhost/login'),
sprintf('the page "%s" does not redirect to http://localhost/login', $url)
);
}
/**
* Provide a client unauthenticated and.
*/
public function getSecuredPagesUnauthenticated()
{
self::bootKernel();
$person = $this->getPersonFromFixtures();
$activities = $this->getActivitiesForPerson($person);
return [
[sprintf('fr/person/%d/activity/', $person->getId())],
[sprintf('fr/person/%d/activity/new', $person->getId())],
[sprintf('fr/person/%d/activity/%d/show', $person->getId(), $activities[0]->getId())],
[sprintf('fr/person/%d/activity/%d/edit', $person->getId(), $activities[0]->getId())],
];
}
/**
* @dataProvider getSecuredPagesAuthenticated
*
* @param type $client
* @param type $url
*/
public function testAccessIsDeniedForUnauthorized($client, $url)
{
$client->request('GET', $url);
$this->assertEquals(403, $client->getResponse()->getStatusCode());
}
public function getSecuredPagesAuthenticated()
{
self::bootKernel();
@@ -55,52 +101,6 @@ final class ActivityControllerTest extends WebTestCase
];
}
/**
* Provide a client unauthenticated and.
*/
public function getSecuredPagesUnauthenticated()
{
self::bootKernel();
$person = $this->getPersonFromFixtures();
$activities = $this->getActivitiesForPerson($person);
return [
[sprintf('fr/person/%d/activity/', $person->getId())],
[sprintf('fr/person/%d/activity/new', $person->getId())],
[sprintf('fr/person/%d/activity/%d/show', $person->getId(), $activities[0]->getId())],
[sprintf('fr/person/%d/activity/%d/edit', $person->getId(), $activities[0]->getId())],
];
}
/**
* @dataProvider getSecuredPagesUnauthenticated
*/
public function testAccessIsDeniedForUnauthenticated(mixed $url)
{
$client = $this->createClient();
$client->request('GET', $url);
$this->assertEquals(302, $client->getResponse()->getStatusCode());
$this->assertTrue(
$client->getResponse()->isRedirect('http://localhost/login'),
sprintf('the page "%s" does not redirect to http://localhost/login', $url)
);
}
/**
* @dataProvider getSecuredPagesAuthenticated
*
* @param type $client
* @param type $url
*/
public function testAccessIsDeniedForUnauthorized($client, $url)
{
$client->request('GET', $url);
$this->assertEquals(403, $client->getResponse()->getStatusCode());
}
public function testCompleteScenario()
{
// Create a new client to browse the application

View File

@@ -137,6 +137,64 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
self::assertIsArray($actual);
}
public function provideDataFindByAccompanyingPeriod(): iterable
{
$this->setUp();
if (null === $period = $this->entityManager
->createQueryBuilder()
->select('a')
->from(AccompanyingPeriod::class, 'a')
->setMaxResults(1)
->getQuery()
->getSingleResult()) {
throw new \RuntimeException('no period found');
}
if ([] === $types = $this->entityManager
->createQueryBuilder()
->select('t')
->from(ActivityType::class, 't')
->setMaxResults(2)
->getQuery()
->getResult()) {
throw new \RuntimeException('no types');
}
if ([] === $jobs = $this->entityManager
->createQueryBuilder()
->select('j')
->from(UserJob::class, 'j')
->setMaxResults(2)
->getQuery()
->getResult()
) {
$job = new UserJob();
$job->setLabel(['fr' => 'test']);
$this->entityManager->persist($job);
$this->entityManager->flush();
}
if (null === $user = $this->entityManager
->createQueryBuilder()
->select('u')
->from(User::class, 'u')
->setMaxResults(1)
->getQuery()
->getSingleResult()
) {
throw new \RuntimeException('no user found');
}
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]];
}
/**
* @dataProvider provideDataFindByPerson
*/
@@ -291,62 +349,4 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]];
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]];
}
public function provideDataFindByAccompanyingPeriod(): iterable
{
$this->setUp();
if (null === $period = $this->entityManager
->createQueryBuilder()
->select('a')
->from(AccompanyingPeriod::class, 'a')
->setMaxResults(1)
->getQuery()
->getSingleResult()) {
throw new \RuntimeException('no period found');
}
if ([] === $types = $this->entityManager
->createQueryBuilder()
->select('t')
->from(ActivityType::class, 't')
->setMaxResults(2)
->getQuery()
->getResult()) {
throw new \RuntimeException('no types');
}
if ([] === $jobs = $this->entityManager
->createQueryBuilder()
->select('j')
->from(UserJob::class, 'j')
->setMaxResults(2)
->getQuery()
->getResult()
) {
$job = new UserJob();
$job->setLabel(['fr' => 'test']);
$this->entityManager->persist($job);
$this->entityManager->flush();
}
if (null === $user = $this->entityManager
->createQueryBuilder()
->select('u')
->from(User::class, 'u')
->setMaxResults(1)
->getQuery()
->getSingleResult()
) {
throw new \RuntimeException('no user found');
}
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]];
}
}

View File

@@ -57,6 +57,46 @@ final class ActivityVoterTest extends KernelTestCase
$this->prophet = new \Prophecy\Prophet();
}
public function testNullUser()
{
$token = $this->prepareToken();
$center = $this->prepareCenter(1, 'center');
$person = $this->preparePerson($center);
$scope = $this->prepareScope(1, 'default');
$activity = $this->prepareActivity($scope, $person);
$this->assertEquals(
VoterInterface::ACCESS_DENIED,
$this->voter->vote($token, $activity, ['CHILL_ACTIVITY_SEE']),
'assert that a null user is not allowed to see'
);
}
/**
* @dataProvider dataProvider_testVoteAction
*
* @param type $expectedResult
* @param string $attribute
* @param string $message
*/
public function testVoteAction(
$expectedResult,
User $user,
Scope $scope,
Center $center,
$attribute,
$message,
) {
$token = $this->prepareToken($user);
$activity = $this->prepareActivity($scope, $this->preparePerson($center));
$this->assertEquals(
$expectedResult,
$this->voter->vote($token, $activity, [$attribute]),
$message
);
}
public function dataProvider_testVoteAction()
{
$centerA = $this->prepareCenter(1, 'center A');
@@ -110,46 +150,6 @@ final class ActivityVoterTest extends KernelTestCase
];
}
public function testNullUser()
{
$token = $this->prepareToken();
$center = $this->prepareCenter(1, 'center');
$person = $this->preparePerson($center);
$scope = $this->prepareScope(1, 'default');
$activity = $this->prepareActivity($scope, $person);
$this->assertEquals(
VoterInterface::ACCESS_DENIED,
$this->voter->vote($token, $activity, ['CHILL_ACTIVITY_SEE']),
'assert that a null user is not allowed to see'
);
}
/**
* @dataProvider dataProvider_testVoteAction
*
* @param type $expectedResult
* @param string $attribute
* @param string $message
*/
public function testVoteAction(
$expectedResult,
User $user,
Scope $scope,
Center $center,
$attribute,
$message,
) {
$token = $this->prepareToken($user);
$activity = $this->prepareActivity($scope, $this->preparePerson($center));
$this->assertEquals(
$expectedResult,
$this->voter->vote($token, $activity, [$attribute]),
$message
);
}
/**
* prepare a token interface with correct rights.
*

View File

@@ -30,6 +30,18 @@ final class AsideActivityControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
/**
* @dataProvider generateAsideActivityId
*/
public function testEditWithoutUsers(int $asideActivityId)
{
self::ensureKernelShutdown();
$client = $this->getClientAuthenticated();
$client->request('GET', "/fr/asideactivity/{$asideActivityId}/edit");
$this->assertEquals(200, $client->getResponse()->getStatusCode());
}
public static function generateAsideActivityId(): iterable
{
self::bootKernel();
@@ -58,18 +70,6 @@ final class AsideActivityControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
/**
* @dataProvider generateAsideActivityId
*/
public function testEditWithoutUsers(int $asideActivityId)
{
self::ensureKernelShutdown();
$client = $this->getClientAuthenticated();
$client->request('GET', "/fr/asideactivity/{$asideActivityId}/edit");
$this->assertEquals(200, $client->getResponse()->getStatusCode());
}
public function testIndexWithoutUsers()
{
self::ensureKernelShutdown();

View File

@@ -42,6 +42,32 @@ final class CalendarControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
/**
* @dataProvider provideAccompanyingPeriod
*/
public function testList(int $accompanyingPeriodId)
{
$this->client->request(
Request::METHOD_GET,
sprintf('/fr/calendar/calendar/by-period/%d', $accompanyingPeriodId)
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
/**
* @dataProvider provideAccompanyingPeriod
*/
public function testNew(int $accompanyingPeriodId)
{
$this->client->request(
Request::METHOD_GET,
sprintf('/fr/calendar/calendar/new?accompanying_period_id=%d', $accompanyingPeriodId)
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
public static function provideAccompanyingPeriod(): iterable
{
self::bootKernel();
@@ -82,30 +108,4 @@ final class CalendarControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
/**
* @dataProvider provideAccompanyingPeriod
*/
public function testList(int $accompanyingPeriodId)
{
$this->client->request(
Request::METHOD_GET,
sprintf('/fr/calendar/calendar/by-period/%d', $accompanyingPeriodId)
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
/**
* @dataProvider provideAccompanyingPeriod
*/
public function testNew(int $accompanyingPeriodId)
{
$this->client->request(
Request::METHOD_GET,
sprintf('/fr/calendar/calendar/new?accompanying_period_id=%d', $accompanyingPeriodId)
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
}

View File

@@ -45,20 +45,6 @@ class MSUserAbsenceReaderTest extends TestCase
self::assertEquals($expected, $absenceReader->isUserAbsent($user), $message);
}
public function testIsUserAbsentWithoutRemoteId(): void
{
$user = new User();
$client = new MockHttpClient();
$mapUser = $this->prophesize(MapCalendarToUser::class);
$mapUser->getUserId($user)->willReturn(null);
$clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00'));
$absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock);
self::assertNull($absenceReader->isUserAbsent($user), 'when no user found, absence should be null');
}
public static function provideDataTestUserAbsence(): iterable
{
// contains data that was retrieved from microsoft graph api on 2023-07-06
@@ -173,4 +159,18 @@ class MSUserAbsenceReaderTest extends TestCase
'User is absent: absence is always enabled',
];
}
public function testIsUserAbsentWithoutRemoteId(): void
{
$user = new User();
$client = new MockHttpClient();
$mapUser = $this->prophesize(MapCalendarToUser::class);
$mapUser->getUserId($user)->willReturn(null);
$clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00'));
$absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock);
self::assertNull($absenceReader->isUserAbsent($user), 'when no user found, absence should be null');
}
}

View File

@@ -28,6 +28,24 @@ use PHPUnit\Framework\TestCase;
*/
final class DefaultRangeGeneratorTest extends TestCase
{
/**
* @dataProvider generateData
*/
public function testGenerateRange(\DateTimeImmutable $date, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate)
{
$generator = new DefaultRangeGenerator();
['startDate' => $actualStartDate, 'endDate' => $actualEndDate] = $generator->generateRange($date);
if (null === $startDate) {
$this->assertNull($actualStartDate);
$this->assertNull($actualEndDate);
} else {
$this->assertEquals($startDate->format(\DateTimeImmutable::ATOM), $actualStartDate->format(\DateTimeImmutable::ATOM));
$this->assertEquals($endDate->format(\DateTimeImmutable::ATOM), $actualEndDate->format(\DateTimeImmutable::ATOM));
}
}
/**
* * Lundi => Envoi des rdv du mardi et mercredi.
* * Mardi => Envoi des rdv du jeudi.
@@ -79,22 +97,4 @@ final class DefaultRangeGeneratorTest extends TestCase
null,
];
}
/**
* @dataProvider generateData
*/
public function testGenerateRange(\DateTimeImmutable $date, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate)
{
$generator = new DefaultRangeGenerator();
['startDate' => $actualStartDate, 'endDate' => $actualEndDate] = $generator->generateRange($date);
if (null === $startDate) {
$this->assertNull($actualStartDate);
$this->assertNull($actualEndDate);
} else {
$this->assertEquals($startDate->format(\DateTimeImmutable::ATOM), $actualStartDate->format(\DateTimeImmutable::ATOM));
$this->assertEquals($endDate->format(\DateTimeImmutable::ATOM), $actualEndDate->format(\DateTimeImmutable::ATOM));
}
}
}

View File

@@ -49,80 +49,6 @@ final class CustomFieldsChoiceTest extends KernelTestCase
parent::tearDown();
}
/**
* provide empty data in different possible representations.
* Those data are supposed to be deserialized.
*
* @return array
*/
public static function emptyDataProvider()
{
return [
// 0
[
// signle
'',
],
// 1
[
// single
null,
],
// 2
[
// signle with allow other
['_other' => 'something', '_choices' => ''],
],
// 3
[
// multiple
[],
],
// 4
[
// multiple with allow other
['_other' => 'something', '_choices' => []],
],
// 5
[
// multiple with allow other
['_other' => '', '_choices' => []],
],
// 6
[
// empty
['_other' => null, '_choices' => null],
],
// 7
[
// empty
[null],
],
];
}
public static function serializedRepresentationDataProvider()
{
return [
[
// multiple => false, allow_other => false
'my-value',
],
[
// multiple => true, allow_ther => false
['my-value'],
],
[
// multiple => false, allow_other => true, current value not in other
['_other' => '', '_choices' => 'my-value'],
],
[
// multiple => true, allow_other => true, current value not in other
['_other' => '', '_choices' => ['my-value']],
],
];
}
/**
* Test if the representation of the data is deserialized to an array text
* with an "allow_other" field.
@@ -412,6 +338,58 @@ final class CustomFieldsChoiceTest extends KernelTestCase
$this->assertTrue($isEmpty);
}
/**
* provide empty data in different possible representations.
* Those data are supposed to be deserialized.
*
* @return array
*/
public static function emptyDataProvider()
{
return [
// 0
[
// signle
'',
],
// 1
[
// single
null,
],
// 2
[
// signle with allow other
['_other' => 'something', '_choices' => ''],
],
// 3
[
// multiple
[],
],
// 4
[
// multiple with allow other
['_other' => 'something', '_choices' => []],
],
// 5
[
// multiple with allow other
['_other' => '', '_choices' => []],
],
// 6
[
// empty
['_other' => null, '_choices' => null],
],
// 7
[
// empty
[null],
],
];
}
// ///////////////////////////////////////
//
// test function isEmptyValue
@@ -435,6 +413,28 @@ final class CustomFieldsChoiceTest extends KernelTestCase
$this->assertFalse($isEmpty);
}
public static function serializedRepresentationDataProvider()
{
return [
[
// multiple => false, allow_other => false
'my-value',
],
[
// multiple => true, allow_ther => false
['my-value'],
],
[
// multiple => false, allow_other => true, current value not in other
['_other' => '', '_choices' => 'my-value'],
],
[
// multiple => true, allow_other => true, current value not in other
['_other' => '', '_choices' => ['my-value']],
],
];
}
/**
* @param array $options
*

View File

@@ -1,7 +1,5 @@
{{ 'docgen.data_dump_email.Dear'|trans }}
{{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }}
{{ 'docgen.data_dump_email.data_dump_ready_and_attached'|trans }}
{{ link }}
{{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }}
{{ 'docgen.data_dump_email.filename'|trans({filename: filename}) }}

View File

@@ -11,13 +11,13 @@ declare(strict_types=1);
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocGeneratorBundle\Service\Generator\Generator;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
@@ -37,15 +37,15 @@ class RequestGenerationHandler implements MessageHandlerInterface
private const LOG_PREFIX = '[docgen message handler] ';
public function __construct(
private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
private readonly DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository,
private readonly EntityManagerInterface $entityManager,
private readonly Generator $generator,
private readonly GeneratorInterface $generator,
private readonly LoggerInterface $logger,
private readonly StoredObjectRepository $storedObjectRepository,
private readonly StoredObjectRepositoryInterface $storedObjectRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly MailerInterface $mailer,
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
private readonly TranslatorInterface $translator,
private readonly StoredObjectManagerInterface $storedObjectManager,
) {}
public function __invoke(RequestGenerationMessage $message)
@@ -90,7 +90,7 @@ class RequestGenerationHandler implements MessageHandlerInterface
$this->sendDataDump($destinationStoredObject, $message);
} else {
$destinationStoredObject = $this->generator->generateDocFromTemplate(
$this->generator->generateDocFromTemplate(
$template,
$message->getEntityId(),
$message->getContextGenerationData(),
@@ -122,19 +122,20 @@ class RequestGenerationHandler implements MessageHandlerInterface
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
{
$url = $this->tempUrlGenerator->generate('GET', $destinationStoredObject->getFilename(), 3600);
$parts = [];
parse_str(parse_url($url->url)['query'], $parts);
$validity = \DateTimeImmutable::createFromFormat('U', $parts['temp_url_expires']);
// Get the content of the document
$content = $this->storedObjectManager->read($destinationStoredObject);
$filename = $destinationStoredObject->getFilename();
$contentType = $destinationStoredObject->getType();
// Create the email with the document as an attachment
$email = (new TemplatedEmail())
->to($message->getSendResultToEmail())
->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig')
->context([
'link' => $url->url,
'validity' => $validity,
'filename' => $filename,
])
->subject($this->translator->trans('docgen.data_dump_email.subject'));
->subject($this->translator->trans('docgen.data_dump_email.subject'))
->attach($content, $filename, $contentType);
$this->mailer->send($email);
}

View File

@@ -0,0 +1,132 @@
<?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\DocGeneratorBundle\Tests\Service\Messenger;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationHandler;
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
*
* @coversNothing
*/
class RequestGenerationHandlerTest extends TestCase
{
use ProphecyTrait;
public function testGenerationHappyScenario(): void
{
// Create entities
$template = new DocGeneratorTemplate();
$this->setPrivateProperty($template, 'id', 1);
$storedObject = new StoredObject();
$this->setPrivateProperty($storedObject, 'id', 2);
$creator = new User();
$creator->setEmail('test@example.com');
$this->setPrivateProperty($creator, 'id', 3);
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
$docGeneratorTemplateRepository->find(1)->willReturn($template);
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
$storedObjectRepository->find(2)->willReturn($storedObject);
$userRepository = $this->prophesize(UserRepositoryInterface::class);
$userRepository->find(3)->willReturn($creator);
// Create a mock for the Query object
$query = $this->prophesize(Query::class);
$query->setParameter('id', 2)->willReturn($query->reveal());
$query->execute()->shouldBeCalled();
// Create a mock for the EntityManager
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->createQuery(Argument::containingString('UPDATE'))->willReturn($query->reveal());
$entityManager->flush()->shouldBeCalled();
$generator = $this->prophesize(GeneratorInterface::class);
$generator->generateDocFromTemplate(
$template,
123, // entityId
['key' => 'value'], // contextGenerationData
$storedObject,
$creator
)
->willReturn($storedObject)->shouldBeCalled();
$logger = new NullLogger();
$mailer = $this->prophesize(MailerInterface::class);
$translator = $this->prophesize(TranslatorInterface::class);
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
// Create handler
$handler = new RequestGenerationHandler(
$docGeneratorTemplateRepository->reveal(),
$entityManager->reveal(),
$generator->reveal(),
$logger,
$storedObjectRepository->reveal(),
$userRepository->reveal(),
$mailer->reveal(),
$translator->reveal(),
$storedObjectManager->reveal()
);
// Create message
$message = new RequestGenerationMessage(
$creator,
$template,
123, // entityId
$storedObject,
['key' => 'value'], // contextGenerationData
false, // isTest
null, // sendResultToEmail
false // dumpOnly
);
// Invoke handler
$handler->__invoke($message);
// Assertions
// The assertions are handled by the shouldBeCalled() expectations on the mocks
$this->assertTrue(true); // Just to have an assertion in the test
}
private function setPrivateProperty(object $object, string $propertyName, $value): void
{
$reflection = new \ReflectionClass($object);
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($object, $value);
}
}

View File

@@ -31,6 +31,36 @@ final class DocGenEncoderTest extends TestCase
$this->encoder = new DocGenEncoder();
}
public function testEmbeddedLoopsThrowsException()
{
$this->expectException(UnexpectedValueException::class);
$data = [
'data' => [
['item' => 'one'],
[
'embedded' => [
[
['subitem' => 'two'],
['subitem' => 'three'],
],
],
],
],
];
$this->encoder->encode($data, 'docgen');
}
/**
* @dataProvider generateEncodeData
*/
public function testEncode(mixed $expected, mixed $data, string $msg)
{
$generated = $this->encoder->encode($data, 'docgen');
$this->assertEquals($expected, $generated, $msg);
}
public static function generateEncodeData()
{
yield [['tests' => 'ok'], ['tests' => 'ok'], 'A simple test with a simple array'];
@@ -93,34 +123,4 @@ final class DocGenEncoderTest extends TestCase
'a longer list, with near real data inside and embedded associative arrays',
];
}
public function testEmbeddedLoopsThrowsException()
{
$this->expectException(UnexpectedValueException::class);
$data = [
'data' => [
['item' => 'one'],
[
'embedded' => [
[
['subitem' => 'two'],
['subitem' => 'three'],
],
],
],
],
];
$this->encoder->encode($data, 'docgen');
}
/**
* @dataProvider generateEncodeData
*/
public function testEncode(mixed $expected, mixed $data, string $msg)
{
$generated = $this->encoder->encode($data, 'docgen');
$this->assertEquals($expected, $generated, $msg);
}
}

View File

@@ -1,4 +1,2 @@
docgen:
data_dump_email:
link_valid_until: >-
Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium}
# No ICU messages needed for data_dump_email anymore

View File

@@ -34,8 +34,10 @@ docgen:
data_dump_email:
subject: Contenu des données de génération de document disponible
Dear: Cher
data_dump_ready_and_link: >-
Le contenu des données est disponible. Vous pouvez le télécharger à l'aide du lien suivant:
data_dump_ready_and_attached: >-
Le contenu des données est disponible. Vous le trouverez en pièce jointe à cet email.
filename: >-
Nom du fichier: %filename%

View File

@@ -85,6 +85,69 @@ class TempUrlLocalStorageGeneratorTest extends TestCase
self::assertEquals($expected, $urlGenerator->validateSignature($signature, $method, $objectName, $expiration), $message);
}
public static function generateValidateSignatureData(): iterable
{
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
true,
'Valid signature, not expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('HEAD', $object_name = 'testABC', $expiration = 1734307200 + 180),
'HEAD',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
true,
'Valid signature, not expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180).'A',
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Invalid signature',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration + 1)),
false,
'Signature expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name.'____',
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Invalid object name',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180),
'POST',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Wrong method',
];
}
/**
* @dataProvider generateValidateSignaturePostData
*/
@@ -164,69 +227,6 @@ class TempUrlLocalStorageGeneratorTest extends TestCase
];
}
public static function generateValidateSignatureData(): iterable
{
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
true,
'Valid signature, not expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('HEAD', $object_name = 'testABC', $expiration = 1734307200 + 180),
'HEAD',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
true,
'Valid signature, not expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180).'A',
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Invalid signature',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration + 1)),
false,
'Signature expired',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('GET', $object_name = 'testABC', $expiration = 1734307200 + 180),
'GET',
$object_name.'____',
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Invalid object name',
];
yield [
TempUrlLocalStorageGeneratorTest::expectedSignature('POST', $object_name = 'testABC', $expiration = 1734307200 + 180),
'POST',
$object_name,
$expiration,
\DateTimeImmutable::createFromFormat('U', (string) ($expiration - 10)),
false,
'Wrong method',
];
}
private function buildGenerator(?UrlGeneratorInterface $urlGenerator = null, ?ClockInterface $clock = null): TempUrlLocalStorageGenerator
{
return new TempUrlLocalStorageGenerator(

View File

@@ -31,6 +31,20 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/
final class StoredObjectManagerTest extends TestCase
{
/**
* @dataProvider getDataProviderForRead
*/
public function testRead(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null)
{
if (null !== $exceptionClass) {
$this->expectException($exceptionClass);
}
$storedObjectManager = $this->getSubject($storedObject, $encodedContent);
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
}
public static function getDataProviderForRead(): \Generator
{
/* HAPPY SCENARIO */
@@ -96,6 +110,40 @@ final class StoredObjectManagerTest extends TestCase
];
}
/**
* @dataProvider getDataProviderForWrite
*/
public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null, ?int $errorCode = null)
{
if (null !== $exceptionClass) {
$this->expectException($exceptionClass);
}
$previousVersion = $storedObject->getCurrentVersion();
$previousFilename = $previousVersion->getFilename();
$client = new MockHttpClient(function ($method, $url, $options) use ($encodedContent, $previousFilename, $errorCode) {
self::assertEquals('PUT', $method);
self::assertStringStartsWith('https://example.com/', $url);
self::assertStringNotContainsString($previousFilename, $url, 'test that the PUT operation is not performed on the same file');
self::assertArrayHasKey('body', $options);
self::assertEquals($encodedContent, $options['body']);
if (-1 === $errorCode) {
throw new TransportException();
}
return new MockResponse('', ['http_code' => $errorCode ?? 201]);
});
$storedObjectManager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject));
$newVersion = $storedObjectManager->write($storedObject, $clearContent);
self::assertNotSame($previousVersion, $newVersion);
self::assertSame($storedObject->getCurrentVersion(), $newVersion);
}
public static function getDataProviderForWrite(): \Generator
{
/* HAPPY SCENARIO */
@@ -150,54 +198,6 @@ final class StoredObjectManagerTest extends TestCase
];
}
/**
* @dataProvider getDataProviderForRead
*/
public function testRead(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null)
{
if (null !== $exceptionClass) {
$this->expectException($exceptionClass);
}
$storedObjectManager = $this->getSubject($storedObject, $encodedContent);
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
}
/**
* @dataProvider getDataProviderForWrite
*/
public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null, ?int $errorCode = null)
{
if (null !== $exceptionClass) {
$this->expectException($exceptionClass);
}
$previousVersion = $storedObject->getCurrentVersion();
$previousFilename = $previousVersion->getFilename();
$client = new MockHttpClient(function ($method, $url, $options) use ($encodedContent, $previousFilename, $errorCode) {
self::assertEquals('PUT', $method);
self::assertStringStartsWith('https://example.com/', $url);
self::assertStringNotContainsString($previousFilename, $url, 'test that the PUT operation is not performed on the same file');
self::assertArrayHasKey('body', $options);
self::assertEquals($encodedContent, $options['body']);
if (-1 === $errorCode) {
throw new TransportException();
}
return new MockResponse('', ['http_code' => $errorCode ?? 201]);
});
$storedObjectManager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject));
$newVersion = $storedObjectManager->write($storedObject, $clearContent);
self::assertNotSame($previousVersion, $newVersion);
self::assertSame($storedObject->getCurrentVersion(), $newVersion);
}
public function testDelete(): void
{
$storedObject = new StoredObject();

View File

@@ -82,6 +82,38 @@ class TempUrlOpenstackGeneratorTest extends KernelTestCase
self::assertEquals($expected, $signedUrl);
}
public static function dataProviderGenerate(): iterable
{
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');
$expireDelay = 1800;
$baseUrls = [
'https://objectstore.example/v1/my_account/container/',
'https://objectstore.example/v1/my_account/container',
];
$objectName = 'object';
$method = 'GET';
$key = 'MYKEY';
$signedUrl = new SignedUrl(
'GET',
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
\DateTimeImmutable::createFromFormat('U', '1702043543'),
$objectName
);
foreach ($baseUrls as $baseUrl) {
yield [
$baseUrl,
$now,
$key,
$method,
$objectName,
$expireDelay,
$signedUrl,
];
}
}
/**
* @dataProvider dataProviderGeneratePost
*/
@@ -125,38 +157,6 @@ class TempUrlOpenstackGeneratorTest extends KernelTestCase
self::assertGreaterThanOrEqual(20, strlen($signedUrl->prefix));
}
public static function dataProviderGenerate(): iterable
{
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');
$expireDelay = 1800;
$baseUrls = [
'https://objectstore.example/v1/my_account/container/',
'https://objectstore.example/v1/my_account/container',
];
$objectName = 'object';
$method = 'GET';
$key = 'MYKEY';
$signedUrl = new SignedUrl(
'GET',
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
\DateTimeImmutable::createFromFormat('U', '1702043543'),
$objectName
);
foreach ($baseUrls as $baseUrl) {
yield [
$baseUrl,
$now,
$key,
$method,
$objectName,
$expireDelay,
$signedUrl,
];
}
}
public static function dataProviderGeneratePost(): iterable
{
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');

View File

@@ -61,6 +61,55 @@ class StoredObjectContentToLocalStorageControllerTest extends TestCase
$controller->contentOperate($request);
}
public static function generateOperateContentWithExceptionDataProvider(): iterable
{
yield [
new Request(['object_name' => '', 'sig' => '', 'exp' => 0]),
BadRequestHttpException::class,
'Object name parameter is missing',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => 0]),
BadRequestHttpException::class,
'Expiration is not set or equal to zero',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
BadRequestHttpException::class,
'Signature is not set or is a blank string',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
AccessDeniedHttpException::class,
'Invalid signature',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
NotFoundHttpException::class,
'Object does not exists on disk',
false,
'',
true,
];
}
public function testOperateContentGetHappyScenario(): void
{
$objectName = 'testABC';
@@ -286,53 +335,4 @@ class StoredObjectContentToLocalStorageControllerTest extends TestCase
'Filename does not start with signed prefix',
];
}
public static function generateOperateContentWithExceptionDataProvider(): iterable
{
yield [
new Request(['object_name' => '', 'sig' => '', 'exp' => 0]),
BadRequestHttpException::class,
'Object name parameter is missing',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => 0]),
BadRequestHttpException::class,
'Expiration is not set or equal to zero',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
BadRequestHttpException::class,
'Signature is not set or is a blank string',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
AccessDeniedHttpException::class,
'Invalid signature',
false,
'',
false,
];
yield [
new Request(['object_name' => 'testABC', 'sig' => '1234', 'exp' => (new \DateTimeImmutable())->getTimestamp()]),
NotFoundHttpException::class,
'Object does not exists on disk',
false,
'',
true,
];
}
}

View File

@@ -136,63 +136,6 @@ class WebdavControllerTest extends KernelTestCase
self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
}
/**
* @dataProvider generateDataPropfindDirectory
*/
public function testPropfindDirectory(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void
{
$controller = $this->buildController();
$request = new Request([], [], [], [], [], [], $requestContent);
$request->setMethod('PROPFIND');
$request->headers->add(['Depth' => '0']);
$response = $controller->propfindDirectory($this->buildDocument(), '1234', $request);
self::assertEquals($expectedStatusCode, $response->getStatusCode());
self::assertContains('content-type', $response->headers->keys());
self::assertStringContainsString('text/xml', $response->headers->get('content-type'));
self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml');
self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
}
public function testHeadDocument(): void
{
$controller = $this->buildController();
$response = $controller->headDocument($this->buildDocument());
self::assertEquals(200, $response->getStatusCode());
self::assertContains('content-length', $response->headers->keys());
self::assertContains('content-type', $response->headers->keys());
self::assertContains('etag', $response->headers->keys());
self::assertEquals('ab56b4d92b40713acc5af89985d4b786', $response->headers->get('etag'));
self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type'));
self::assertEquals(5, $response->headers->get('content-length'));
}
public function testPutDocument(): void
{
$document = $this->buildDocument();
$entityManager = $this->createMock(EntityManagerInterface::class);
$storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);
// entity manager must be flushed
$entityManager->expects($this->once())
->method('flush');
// object must be written by StoredObjectManager
$storedObjectManager->expects($this->once())
->method('write')
->with($this->identicalTo($document), $this->identicalTo('1234'));
$controller = $this->buildController($entityManager, $storedObjectManager);
$request = new Request(content: '1234');
$response = $controller->putDocument($document, $request);
self::assertEquals(204, $response->getStatusCode());
self::assertEquals('', $response->getContent());
}
public static function generateDataPropfindDocument(): iterable
{
$content =
@@ -347,6 +290,25 @@ class WebdavControllerTest extends KernelTestCase
];
}
/**
* @dataProvider generateDataPropfindDirectory
*/
public function testPropfindDirectory(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void
{
$controller = $this->buildController();
$request = new Request([], [], [], [], [], [], $requestContent);
$request->setMethod('PROPFIND');
$request->headers->add(['Depth' => '0']);
$response = $controller->propfindDirectory($this->buildDocument(), '1234', $request);
self::assertEquals($expectedStatusCode, $response->getStatusCode());
self::assertContains('content-type', $response->headers->keys());
self::assertStringContainsString('text/xml', $response->headers->get('content-type'));
self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml');
self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
}
public static function generateDataPropfindDirectory(): iterable
{
yield [
@@ -414,6 +376,44 @@ class WebdavControllerTest extends KernelTestCase
'test creatableContentsInfo',
];
}
public function testHeadDocument(): void
{
$controller = $this->buildController();
$response = $controller->headDocument($this->buildDocument());
self::assertEquals(200, $response->getStatusCode());
self::assertContains('content-length', $response->headers->keys());
self::assertContains('content-type', $response->headers->keys());
self::assertContains('etag', $response->headers->keys());
self::assertEquals('ab56b4d92b40713acc5af89985d4b786', $response->headers->get('etag'));
self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type'));
self::assertEquals(5, $response->headers->get('content-length'));
}
public function testPutDocument(): void
{
$document = $this->buildDocument();
$entityManager = $this->createMock(EntityManagerInterface::class);
$storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);
// entity manager must be flushed
$entityManager->expects($this->once())
->method('flush');
// object must be written by StoredObjectManager
$storedObjectManager->expects($this->once())
->method('write')
->with($this->identicalTo($document), $this->identicalTo('1234'));
$controller = $this->buildController($entityManager, $storedObjectManager);
$request = new Request(content: '1234');
$response = $controller->putDocument($document, $request);
self::assertEquals(204, $response->getStatusCode());
self::assertEquals('', $response->getContent());
}
}
class MockedStoredObjectManager implements StoredObjectManagerInterface

View File

@@ -87,6 +87,16 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase
self::assertIsInt($nb, 'test that the query could be executed');
}
public static function provideDataBuildFetchQueryForPerson(): iterable
{
yield [null, null, null];
yield [new \DateTimeImmutable('1 year ago'), null, null];
yield [null, new \DateTimeImmutable('1 year ago'), null];
yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), null];
yield [null, null, 'test'];
yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), 'test'];
}
/**
* @dataProvider provideDateForFetchQueryForAccompanyingPeriod
*/
@@ -142,14 +152,4 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase
yield [$period, null, null, 'test'];
yield [$period, new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), 'test'];
}
public static function provideDataBuildFetchQueryForPerson(): iterable
{
yield [null, null, null];
yield [new \DateTimeImmutable('1 year ago'), null, null];
yield [null, new \DateTimeImmutable('1 year ago'), null];
yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), null];
yield [null, null, 'test'];
yield [new \DateTimeImmutable('2 years ago'), new \DateTimeImmutable('1 year ago'), 'test'];
}
}

View File

@@ -50,19 +50,6 @@ class StoredObjectVoterTest extends TestCase
self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
}
private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface
{
$storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class);
$storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports')
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class))
->willReturn($supports);
$storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute')
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class))
->willReturn($voteOnAttribute);
return $storedObjectVoter;
}
public static function provideDataVote(): iterable
{
yield [
@@ -120,4 +107,17 @@ class StoredObjectVoterTest extends TestCase
VoterInterface::ACCESS_GRANTED,
];
}
private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface
{
$storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class);
$storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports')
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class))
->willReturn($supports);
$storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute')
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class))
->willReturn($voteOnAttribute);
return $storedObjectVoter;
}
}

View File

@@ -40,29 +40,6 @@ class RemoveOldVersionCronJobTest extends KernelTestCase
self::assertEquals($expected, $cronJob->canRun($cronJobExecution));
}
public function testRun(): void
{
// we create a clock in the future. This led us a chance to having stored object to delete
$clock = new MockClock(new \DateTimeImmutable('2024-01-01 00:00:00', new \DateTimeZone('+00:00')));
$repository = $this->createMock(StoredObjectVersionRepository::class);
$repository->expects($this->once())
->method('findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime')
->with(new \DateTime('2023-10-03 00:00:00', new \DateTimeZone('+00:00')))
->willReturnCallback(function ($arg) {
yield 1;
yield 3;
yield 2;
})
;
$cronJob = new RemoveOldVersionCronJob($clock, $this->buildMessageBus(true), $repository);
$results = $cronJob->run([]);
self::assertArrayHasKey('last-deleted-stored-object-version-id', $results);
self::assertIsInt($results['last-deleted-stored-object-version-id']);
}
public static function buildTestCanRunData(): iterable
{
yield [
@@ -86,6 +63,29 @@ class RemoveOldVersionCronJobTest extends KernelTestCase
];
}
public function testRun(): void
{
// we create a clock in the future. This led us a chance to having stored object to delete
$clock = new MockClock(new \DateTimeImmutable('2024-01-01 00:00:00', new \DateTimeZone('+00:00')));
$repository = $this->createMock(StoredObjectVersionRepository::class);
$repository->expects($this->once())
->method('findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime')
->with(new \DateTime('2023-10-03 00:00:00', new \DateTimeZone('+00:00')))
->willReturnCallback(function ($arg) {
yield 1;
yield 3;
yield 2;
})
;
$cronJob = new RemoveOldVersionCronJob($clock, $this->buildMessageBus(true), $repository);
$results = $cronJob->run([]);
self::assertArrayHasKey('last-deleted-stored-object-version-id', $results);
self::assertIsInt($results['last-deleted-stored-object-version-id']);
}
private function buildMessageBus(bool $expectDistpatchAtLeastOnce = false): MessageBusInterface
{
$messageBus = $this->createMock(MessageBusInterface::class);

View File

@@ -48,30 +48,6 @@ class EventTypeController extends AbstractController
]);
}
/**
* Deletes a EventType entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/event_type/{id}/delete', name: 'chill_eventtype_admin_delete', methods: ['POST', 'DELETE'])]
public function deleteAction(Request $request, mixed $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(EventType::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find EventType entity.');
}
$em->remove($entity);
$em->flush();
}
return $this->redirectToRoute('chill_eventtype_admin');
}
/**
* Displays a form to edit an existing EventType entity.
*/
@@ -87,12 +63,10 @@ class EventTypeController extends AbstractController
}
$editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/EventType/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -126,28 +100,6 @@ class EventTypeController extends AbstractController
]);
}
/**
* Finds and displays a EventType entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/event_type/{id}/show', name: 'chill_eventtype_admin_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(EventType::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find EventType entity.');
}
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/EventType/show.html.twig', [
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Edits an existing EventType entity.
*/
@@ -162,7 +114,6 @@ class EventTypeController extends AbstractController
throw $this->createNotFoundException('Unable to find EventType entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
@@ -175,7 +126,6 @@ class EventTypeController extends AbstractController
return $this->render('@ChillEvent/EventType/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -198,22 +148,6 @@ class EventTypeController extends AbstractController
return $form;
}
/**
* Creates a form to delete a EventType entity by id.
*
* @return \Symfony\Component\Form\FormInterface The form
*/
private function createDeleteForm(mixed $id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl(
'chill_eventtype_admin_delete',
['id' => $id]
))
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
}
/**
* Creates a form to edit a EventType entity.
*
@@ -228,7 +162,7 @@ class EventTypeController extends AbstractController
'chill_eventtype_admin_update',
['id' => $entity->getId()]
),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -48,30 +48,6 @@ class RoleController extends AbstractController
]);
}
/**
* Deletes a Role entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/role/{id}/delete', name: 'chill_event_admin_role_delete', methods: ['POST', 'DELETE'])]
public function deleteAction(Request $request, mixed $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Role::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Role entity.');
}
$em->remove($entity);
$em->flush();
}
return $this->redirectToRoute('chill_event_admin_role');
}
/**
* Displays a form to edit an existing Role entity.
*/
@@ -87,12 +63,10 @@ class RoleController extends AbstractController
}
$editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Role/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -126,28 +100,6 @@ class RoleController extends AbstractController
]);
}
/**
* Finds and displays a Role entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/role/{id}/show', name: 'chill_event_admin_role_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Role::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Role entity.');
}
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Role/show.html.twig', [
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Edits an existing Role entity.
*/
@@ -162,7 +114,6 @@ class RoleController extends AbstractController
throw $this->createNotFoundException('Unable to find Role entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
@@ -175,7 +126,6 @@ class RoleController extends AbstractController
return $this->render('@ChillEvent/Role/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -198,20 +148,6 @@ class RoleController extends AbstractController
return $form;
}
/**
* Creates a form to delete a Role entity by id.
*
* @return \Symfony\Component\Form\FormInterface The form
*/
private function createDeleteForm(mixed $id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('chill_event_admin_role_delete', ['id' => $id]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
}
/**
* Creates a form to edit a Role entity.
*
@@ -226,7 +162,7 @@ class RoleController extends AbstractController
'chill_event_admin_role_update',
['id' => $entity->getId()]
),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -48,30 +48,6 @@ class StatusController extends AbstractController
]);
}
/**
* Deletes a Status entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/status/{id}/delete', name: 'chill_event_admin_status_delete', methods: ['POST', 'DELETE'])]
public function deleteAction(Request $request, mixed $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Status::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Status entity.');
}
$em->remove($entity);
$em->flush();
}
return $this->redirectToRoute('chill_event_admin_status');
}
/**
* Displays a form to edit an existing Status entity.
*/
@@ -87,12 +63,10 @@ class StatusController extends AbstractController
}
$editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Status/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -126,28 +100,6 @@ class StatusController extends AbstractController
]);
}
/**
* Finds and displays a Status entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/status/{id}/show', name: 'chill_event_admin_status_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Status::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Status entity.');
}
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Status/show.html.twig', [
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Edits an existing Status entity.
*/
@@ -162,7 +114,6 @@ class StatusController extends AbstractController
throw $this->createNotFoundException('Unable to find Status entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
@@ -175,7 +126,6 @@ class StatusController extends AbstractController
return $this->render('@ChillEvent/Status/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@@ -198,19 +148,6 @@ class StatusController extends AbstractController
return $form;
}
/**
* Creates a form to delete a Status entity by id.
*
* @return \Symfony\Component\Form\FormInterface The form
*/
private function createDeleteForm(mixed $id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('chill_event_admin_status_delete', ['id' => $id]))
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
}
/**
* Creates a form to edit a Status entity.
*
@@ -222,7 +159,7 @@ class StatusController extends AbstractController
{
$form = $this->createForm(StatusType::class, $entity, [
'action' => $this->generateUrl('chill_event_admin_status_update', ['id' => $entity->getId()]),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@@ -8,7 +8,7 @@
{{ form_row(edit_form.name) }}
{{ form_row(edit_form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_eventtype_admin') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@@ -16,14 +16,11 @@
<tbody>
{% for entity in entities %}
<tr>
<td><a href="{{ path('chill_eventtype_admin_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
<td>{{ entity.id }}</a></td>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.active }}</td>
<td><i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i></td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_eventtype_admin_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_eventtype_admin_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>

View File

@@ -8,7 +8,7 @@
{{ form_row(form.name) }}
{{ form_row(form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_eventtype_admin') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@@ -21,17 +21,12 @@
</tbody>
</table>
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_eventtype_admin') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
<a href="{{ path('chill_eventtype_admin_edit', { 'id': entity.id }) }}" class="btn btn-edit">{{ 'Edit'|trans }}</a>
</li>
<li>
{{ form_start(delete_form) }}
{{ form_row(delete_form.submit, { 'attr': { 'class' : 'btn btn-delete' }}) }}
{{ form_end(delete_form) }}
</li>
</ul>
{% endblock %}

View File

@@ -8,12 +8,12 @@
{{ form_row(edit_form.type) }}
{{ form_row(edit_form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_role') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
{{ form_row(edit_form.submit, { 'attr': { 'class' : 'btn btn-edit' }}) }}
{{ form_row(edit_form.submit, { 'attr': { 'class' : 'btn btn-update' }}) }}
</li>
</ul>

View File

@@ -17,15 +17,12 @@
<tbody>
{% for entity in entities %}
<tr>
<td><a href="{{ path('chill_event_admin_role_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
<td>{{ entity.id }}</a></td>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.type.name|localize_translatable_string }}</td>
<td>{{ entity.active }}</td>
<td><i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i></td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_event_admin_role_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_event_admin_role_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>

View File

@@ -9,7 +9,7 @@
{{ form_row(form.type) }}
{{ form_row(form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_role') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@@ -25,17 +25,12 @@
</tbody>
</table>
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_role') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
<a href="{{ path('chill_event_admin_role_edit', { 'id': entity.id }) }}" class="btn btn-edit">{{ 'Edit'|trans }}</a>
</li>
<li>
{{ form_start(delete_form) }}
{{ form_row(delete_form.submit, { 'attr': { 'class' : 'btn btn-delete' }}) }}
{{ form_end(delete_form) }}
</li>
</ul>
{% endblock %}

View File

@@ -9,7 +9,7 @@
{{ form_row(edit_form.type) }}
{{ form_row(edit_form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_status') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@@ -17,15 +17,12 @@
<tbody>
{% for entity in entities %}
<tr>
<td><a href="{{ path('chill_event_admin_status_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
<td>{{ entity.id }}</a></td>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.type.name|localize_translatable_string }}</td>
<td>{{ entity.active }}</td>
<td><i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i></td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_event_admin_status_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_event_admin_status_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>

View File

@@ -9,7 +9,7 @@
{{ form_row(form.type) }}
{{ form_row(form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_status') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@@ -25,17 +25,12 @@
</tbody>
</table>
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_status') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
<a href="{{ path('chill_event_admin_status_edit', { 'id': entity.id }) }}" class="btn btn-edit">{{ 'Edit'|trans }}</a>
</li>
<li>
{{ form_start(delete_form) }}
{{ form_row(delete_form.submit, { 'attr': { 'class' : 'btn btn-delete' }}) }}
{{ form_end(delete_form) }}
</li>
</ul>
{% endblock %}

View File

@@ -11,10 +11,8 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Entity\NotificationFlagEnum;
use Chill\MainBundle\Form\NotificationCommentType;
use Chill\MainBundle\Form\NotificationType;
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
@@ -24,9 +22,6 @@ use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Security\Authorization\NotificationVoter;
use Chill\MainBundle\Security\ChillSecurity;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -58,29 +53,11 @@ class NotificationController extends AbstractController
throw new BadRequestHttpException('missing entityId parameter');
}
$notificationType = '';
switch ($request->query->get('entityClass')) {
case Activity::class:
$notificationType = NotificationFlagEnum::ACTIVITY;
break;
case AccompanyingPeriod::class:
$notificationType = NotificationFlagEnum::ACC_COURSE;
break;
case AccompanyingPeriodWork::class:
$notificationType = NotificationFlagEnum::ACC_COURSE_WORK;
break;
case AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument::class:
$notificationType = NotificationFlagEnum::ACC_COURSE_WORK_EVAL_DOC;
break;
}
$notification = new Notification();
$notification
->setRelatedEntityClass($request->query->get('entityClass'))
->setRelatedEntityId($request->query->getInt('entityId'))
->setSender($this->security->getUser())
->setType($notificationType);
->setSender($this->security->getUser());
$tos = $request->query->all('tos');

View File

@@ -11,12 +11,14 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Form\UserProfileType;
use Chill\MainBundle\Form\UserPhonenumberType;
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;
@@ -39,21 +41,16 @@ final class UserProfileController extends AbstractController
}
$user = $this->security->getUser();
$editForm = $this->createForm(UserProfileType::class, $user);
$editForm->get('notificationFlags')->setData($user->getNotificationFlags());
$editForm->add('submit', SubmitType::class);
$editForm = $this->createPhonenumberEditForm($user);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$notificationFlagsData = $editForm->get('notificationFlags')->getData();
$user->setNotificationFlags($notificationFlagsData);
$phonenumber = $editForm->get('phonenumber')->getData();
$em = $this->managerRegistry->getManager();
$em->persist($user);
$em->flush();
$this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!'));
$user->setPhonenumber($phonenumber);
$this->managerRegistry->getManager()->flush();
$this->addFlash('success', $this->translator->trans('user.profile.Phonenumber successfully updated!'));
return $this->redirectToRoute('chill_main_user_profile');
}
@@ -63,4 +60,13 @@ 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

@@ -70,9 +70,9 @@ class NewsItem implements TrackCreationInterface, TrackUpdateInterface
return $this->content;
}
public function setContent(string $content): void
public function setContent(?string $content): void
{
$this->content = $content;
$this->content = (string) $content;
}
public function getStartDate(): ?\DateTimeImmutable

View File

@@ -14,7 +14,6 @@ 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;
@@ -25,7 +24,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Index(name: 'chill_main_notification_related_entity_idx', columns: ['relatedentityclass', 'relatedentityid'])]
class Notification implements TrackUpdateInterface
{
#[ORM\Column(type: Types::TEXT, nullable: false)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false)]
private string $accessKey;
private array $addedAddresses = [];
@@ -42,7 +41,7 @@ class Notification implements TrackUpdateInterface
*
* @var array|string[]
*/
#[ORM\Column(type: Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
private array $addressesEmails = [];
/**
@@ -61,21 +60,21 @@ class Notification implements TrackUpdateInterface
#[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
private Collection $comments;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
private \DateTimeImmutable $date;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
#[ORM\Column(type: Types::TEXT)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
private string $message = '';
#[ORM\Column(type: Types::STRING, length: 255)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
private string $relatedEntityClass = '';
#[ORM\Column(type: Types::INTEGER)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private int $relatedEntityId;
private array $removedAddresses = [];
@@ -85,7 +84,7 @@ class Notification implements TrackUpdateInterface
private ?User $sender = null;
#[Assert\NotBlank(message: 'notification.Title must be defined')]
#[ORM\Column(type: Types::TEXT, options: ['default' => ''])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
private string $title = '';
/**
@@ -95,15 +94,12 @@ class Notification implements TrackUpdateInterface
#[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
private Collection $unreadBy;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
#[ORM\Column(type: \Doctrine\DBAL\Types\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, enumType: NotificationFlagEnum::class)]
private NotificationFlagEnum $type;
public function __construct()
{
$this->addressees = new ArrayCollection();
@@ -393,16 +389,4 @@ class Notification implements TrackUpdateInterface
return $this;
}
public function setType(NotificationFlagEnum $type): self
{
$this->type = $type;
return $this;
}
public function getType(): NotificationFlagEnum
{
return $this->type;
}
}

View File

@@ -1,23 +0,0 @@
<?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\Entity;
enum NotificationFlagEnum: string
{
case REFERRER_ACC_COURSE = 'referrer-acc-course-notif';
case PERSON_MOVE = 'person-move-notif';
case ACC_COURSE = 'acc-course-notif';
case WORKFLOW_TRANS = 'workflow-trans-notif';
case ACC_COURSE_WORK = 'acc-course-work-notif';
case ACC_COURSE_WORK_EVAL_DOC = 'acc-course-work-eval-doc-notif';
case ACTIVITY = 'activity-notif';
}

View File

@@ -116,9 +116,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
#[PhonenumberConstraint]
private ?PhoneNumber $phonenumber = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
private $notificationFlags = [];
/**
* User constructor.
*/
@@ -616,24 +613,4 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this;
}
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;
}
}

View File

@@ -1,78 +0,0 @@
<?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 Symfony\Component\Form\DataMapperInterface;
final readonly class NotificationFlagDataMapper implements DataMapperInterface
{
private array $notificationFlagProviders;
public function __construct(array $notificationFlagProviders)
{
$this->notificationFlagProviders = $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('immediate-email', $viewData[$flag] ?? []);
$dailyEmailChecked = in_array('daily-email', $viewData[$flag] ?? []);
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 ($flagForm['immediate_email']->getData()) {
$viewData[$flag][] = 'immediate-email';
}
if ($flagForm['daily_email']->getData()) {
$viewData[$flag][] = 'daily-email';
}
if (empty($viewData[$flag])) {
$viewData[$flag][] = 'no-email';
}
}
}
}
}

View File

@@ -1,62 +0,0 @@
<?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 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, // Keep this here for the individual checkboxes
])
->add('daily_email', CheckboxType::class, [
'label' => false,
'required' => false,
'mapped' => false, // Keep this here for the individual checkboxes
]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}

View File

@@ -1,41 +0,0 @@
<?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

@@ -1,70 +0,0 @@
<?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\ScheduleDailyNotificationEmailMessage;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendDailyDigestMessage;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Symfony\Contracts\Cache\CacheInterface;
readonly class ScheduleDailyNotificationEmailHandler
{
public function __construct(
private CacheInterface $dailyNotificationsCache,
private MessageBusInterface $messageBus,
private LoggerInterface $logger,
) {}
/**
* @throws InvalidArgumentException
*/
public function __invoke(ScheduleDailyNotificationEmailMessage $message): void
{
$userId = $message->getAddresseeId();
$notificationId = $message->getNotificationId();
// Store notification in cache grouped by user
$cacheKey = "daily_notifications_user_{$userId}";
$existingNotifications = $this->dailyNotificationsCache->get($cacheKey, function () {
return [];
});
$existingNotifications[] = $notificationId;
$this->dailyNotificationsCache->get($cacheKey, function () use ($existingNotifications) {
return $existingNotifications;
});
// Only send the daily digest message if this is the first notification for today otherwise it already exists
if (1 === count($existingNotifications)) {
$digestMessage = new SendDailyDigestMessage($userId);
// Calculate delay until next 9 AM
$now = new \DateTimeImmutable();
$nextNineAM = $now->modify('tomorrow 09:00');
$delay = $nextNineAM->getTimestamp() - $now->getTimestamp();
$this->messageBus->dispatch($digestMessage, [
new DelayStamp($delay * 1000),
]);
}
$this->logger->info('[ScheduleDailyNotificationEmailHandler] Added notification to daily cache', [
'notification_id' => $notificationId,
'user_id' => $userId,
'total_pending' => count($existingNotifications),
]);
}
}

View File

@@ -1,66 +0,0 @@
<?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\SendDailyDigestMessage;
use Chill\MainBundle\Notification\Email\NotificationMailer;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Repository\UserRepository;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\CacheInterface;
#[AsMessageHandler]
class SendDailyDigestHandler
{
public function __construct(
private readonly CacheInterface $dailyNotificationsCache,
private readonly NotificationRepository $notificationRepository,
private readonly UserRepository $userRepository,
private readonly NotificationMailer $notificationMailer,
private readonly LoggerInterface $logger,
) {}
/**
* @throws InvalidArgumentException
*/
public function __invoke(SendDailyDigestMessage $message): void
{
$userId = $message->getUserId();
$cacheKey = "daily_notifications_user_{$userId}";
$notificationIds = $this->dailyNotificationsCache->get($cacheKey, []);
if (empty($notificationIds)) {
$this->logger->info('[SendDailyDigestHandler] No notifications found for user', [
'user_id' => $userId,
]);
return;
}
$user = $this->userRepository->find($userId);
$notifications = $this->notificationRepository->findBy(['id' => $notificationIds]);
if ($user && !empty($notifications)) {
$this->notificationMailer->sendDailyDigest($user, $notifications);
// Clear the cache after sending
$this->dailyNotificationsCache->delete($cacheKey);
$this->logger->info('[SendDailyDigestHandler] Sent daily digest', [
'user_id' => $userId,
'notification_count' => count($notifications),
]);
}
}
}

View File

@@ -1,68 +0,0 @@
<?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]
class SendImmediateNotificationEmailHandler
{
public function __construct(
private readonly NotificationRepository $notificationRepository,
private readonly UserRepository $userRepository,
private readonly NotificationMailer $notificationMailer,
private readonly 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(),
]);
return;
}
if (null === $addressee) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [
'addressee_id' => $message->getAddresseeId(),
]);
return;
}
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(),
'error' => $e->getMessage(),
]);
throw $e;
}
}
}

View File

@@ -1,30 +0,0 @@
<?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 ScheduleDailyNotificationEmailMessage
{
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

@@ -1,24 +0,0 @@
<?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;
class SendDailyDigestMessage
{
public function __construct(
private readonly int $userId,
) {}
public function getUserId(): int
{
return $this->userId;
}
}

View File

@@ -1,30 +0,0 @@
<?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,8 +13,6 @@ namespace Chill\MainBundle\Notification\Email;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationEmailMessage;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PostUpdateEventArgs;
use Psr\Log\LoggerInterface;
@@ -22,12 +20,11 @@ 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;
readonly class NotificationMailer
class NotificationMailer
{
public function __construct(private MailerInterface $mailer, private LoggerInterface $logger, private MessageBusInterface $messageBus, private readonly TranslatorInterface $translator) {}
public function __construct(private readonly MailerInterface $mailer, private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator) {}
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
{
@@ -83,152 +80,38 @@ readonly class NotificationMailer
private function sendNotificationEmailsToAddresses(Notification $notification): void
{
if (null === $notification->getType()) {
$this->logger->warning('[NotificationMailer] Notification has no type, skipping email processing', [
'notification_id' => $notification->getId(),
]);
return;
}
foreach ($notification->getAddressees() as $addressee) {
if (null === $addressee->getEmail()) {
continue;
}
$this->processNotificationForAddressee($notification, $addressee);
}
}
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,
]);
}
private function processNotificationForAddressee(Notification $notification, $addressee): void
{
$notificationFlags = $addressee->getNotificationFlags();
$notificationType = $notification->getType();
$emailPreference = $notificationFlags[$notificationType->value] ?? null;
match ($emailPreference) {
'immediate-email' => $this->scheduleImmediateEmail($notification, $addressee),
'daily-email' => $this->scheduleDailyEmail($notification, $addressee),
default => $this->logger->debug('[NotificationMailer] No email preference set for notification type', [
'notification_id' => $notification->getId(),
'addressee_email' => $addressee->getEmail(),
'notification_type' => $notificationType->value,
'preference' => $emailPreference,
]),
};
}
private function scheduleImmediateEmail(Notification $notification, $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(),
]);
}
private function scheduleDailyEmail(Notification $notification, $addressee): void
{
$message = new ScheduleDailyNotificationEmailMessage(
$notification->getId(),
$addressee->getId()
);
$this->messageBus->dispatch($message);
$this->logger->info('[NotificationMailer] Scheduled daily 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, $addressee): void
{
if (null === $addressee->getEmail()) {
return;
}
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,
->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(),
]);
}
$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, array $notifications): void
{
if (null === $user->getEmail() || empty($notifications)) {
return;
}
$email = new TemplatedEmail();
$email
->textTemplate('@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; // Re-throw so the message handler can handle the failure
}
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace Chill\MainBundle\Notification\FlagProviders;
use Chill\MainBundle\Entity\NotificationFlagEnum;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class AccompanyingCourseNotificationFlagProvider implements NotificationFlagProviderInterface
{
public function getFlag(): string
{
return NotificationFlagEnum::ACC_COURSE->value;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.acc-course');
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace Chill\MainBundle\Notification\FlagProviders;
use Chill\MainBundle\Entity\NotificationFlagEnum;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class AccompanyingCourseWorkEvalDocNotificationFlagProvider implements NotificationFlagProviderInterface
{
public function getFlag(): string
{
return NotificationFlagEnum::ACC_COURSE_WORK_EVAL_DOC->value;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.acc-course-work-eval-doc');
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace Chill\MainBundle\Notification\FlagProviders;
use Chill\MainBundle\Entity\NotificationFlagEnum;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class AccompanyingCourseWorkNotificationFlagProvider implements NotificationFlagProviderInterface
{
public function getFlag(): string
{
return NotificationFlagEnum::ACC_COURSE_WORK->value;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.acc-course-work');
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace Chill\MainBundle\Notification\FlagProviders;
use Chill\MainBundle\Entity\NotificationFlagEnum;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class ActivityNotificationFlagProvider implements NotificationFlagProviderInterface
{
public function getFlag(): string
{
return NotificationFlagEnum::ACTIVITY->value;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.activity');
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace Chill\MainBundle\Notification\FlagProviders;
use Chill\MainBundle\Entity\NotificationFlagEnum;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class DesignatedReferrerNotificationFlagProvider implements NotificationFlagProviderInterface
{
public function getFlag(): string
{
return NotificationFlagEnum::REFERRER_ACC_COURSE->value;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.referrer-acc-course');
}
}

View File

@@ -1,23 +0,0 @@
<?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;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('chill_main.notification_flag_provider')]
interface NotificationFlagProviderInterface
{
public function getFlag(): string;
public function getLabel(): TranslatableInterface;
}

View File

@@ -1,21 +0,0 @@
<?php
namespace Chill\MainBundle\Notification\FlagProviders;
use Chill\MainBundle\Entity\NotificationFlagEnum;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class PersonAddressMoveNotificationFlagProvider implements NotificationFlagProviderInterface
{
public function getFlag(): string
{
return NotificationFlagEnum::PERSON_MOVE->value;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.person-address-move');
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace Chill\MainBundle\Notification\FlagProviders;
use Chill\MainBundle\Entity\NotificationFlagEnum;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class WorkflowTransitionNotificationFlagProvider implements NotificationFlagProviderInterface
{
public function getFlag(): string
{
return NotificationFlagEnum::WORKFLOW_TRANS->value;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.workflow-trans');
}
}

View File

@@ -1,44 +0,0 @@
<?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;
}
public function getNotificationFlagProviderByLabel(string $label): ?NotificationFlagProviderInterface
{
foreach ($this->notificationFlagProviders as $provider) {
if ($provider->getLabel() == $label) {
return $provider;
}
}
return null;
}
}

View File

@@ -2,10 +2,10 @@
<span class="chill-entity entity-user">
{{ user.label }}
<span class="user-job" v-if="user.user_job !== null"
>({{ localizeString(user.user_job.label) }})</span
> ({{ localizeString(user.user_job.label) }})</span
>
<span class="main-scope" v-if="user.main_scope !== null"
>({{ localizeString(user.main_scope.name) }})</span
> ({{ localizeString(user.main_scope.name) }})</span
>
<span
v-if="user.isAbsent"
@@ -22,8 +22,8 @@ import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizat
export default {
name: "UserRenderBoxBadge",
methods: {
localizeString() {
return localizeString;
localizeString(label) {
return localizeString(label);
},
},
props: ["user"],

View File

@@ -1,18 +0,0 @@
# Résumé quotidien des notifications
Bonjour {{ user.name ?? user.email }},
Voici vos {{ notification_count }} notification{% if notification_count > 1 %}s{% endif %} du jour :
{% for notification in notifications %}
## {{ notification.title }}
{{ notification.message }}
Vous pouvez visualiser la notification et y répondre ici:
{{ absolute_url(path('chill_main_notification_show', {'_locale': 'fr', 'id': notification.id }, false)) }}
{% endfor %}
--
Le logiciel Chill

View File

@@ -45,32 +45,6 @@
{{ form_start(form) }}
{{ form_row(form.phonenumber) }}
<h2 class="mb-4">{{ 'user.profile.notification_preferences'|trans }}</h2>
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'notification.flags.type'|trans }}</th>
<th>{{ 'notification.flags.preferences.immediate_email'|trans }}</th>
<th>{{ 'notification.flags.preferences.daily_email'|trans }}</th>
</tr>
</thead>
<tbody>
{% for flag in form.notificationFlags %}
<tr>
<td class="col-sm-6">
<label>{{ form_label(flag) }}</label>
</td>
<td>
{{ form_widget(flag.immediate_email) }}
</td>
<td>
{{ form_widget(flag.daily_email) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<ul class="record_actions">
<li>
{{ form_widget(form.submit, { 'attr': { 'class': 'btn btn-save' } } ) }}

View File

@@ -58,7 +58,6 @@ final readonly class CancelStaleWorkflowHandler
$transitions = $workflowComponent->getEnabledTransitions($workflow);
$transitionApplied = false;
$wasInInitialPosition = 'initial' === $workflow->getStep();
foreach ($transitions as $transition) {
if ($this->willTransitionLeadToFinalNegative($transition, $metadataStore)) {
@@ -80,10 +79,6 @@ final readonly class CancelStaleWorkflowHandler
throw new UnrecoverableMessageHandlingException(sprintf('No valid transition found for EntityWorkflow %d.', $workflowId));
}
if ($wasInInitialPosition) {
$this->em->remove($workflow);
}
$this->em->flush();
}

View File

@@ -32,107 +32,6 @@ abstract class AbstractAggregatorTest extends KernelTestCase
self::ensureKernelShutdown();
}
/**
* provide data for `testAliasDidNotDisappears`.
*/
public static function dataProviderAliasDidNotDisappears()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* provide data for `testAlterQuery`.
*/
public static function dataProviderAlterQuery()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
public static function dataProviderQueryExecution(): iterable
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* prepare data for `testGetQueryKeys`.
*/
public static function dataProviderGetQueryKeys()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach ($datas as $data) {
yield [$data];
}
}
/**
* prepare date for method `testGetResultsAndLabels`.
*/
public static function dataProviderGetResultsAndLabels()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* Create an aggregator instance which will be used in tests.
*
@@ -187,6 +86,28 @@ abstract class AbstractAggregatorTest extends KernelTestCase
}
}
/**
* provide data for `testAliasDidNotDisappears`.
*/
public static function dataProviderAliasDidNotDisappears()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* @dataProvider dataProviderQueryExecution
*
@@ -201,6 +122,25 @@ abstract class AbstractAggregatorTest extends KernelTestCase
self::assertIsArray($actual);
}
public static function dataProviderQueryExecution(): iterable
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* test the alteration of query by the filter.
*
@@ -239,6 +179,28 @@ abstract class AbstractAggregatorTest extends KernelTestCase
);
}
/**
* provide data for `testAlterQuery`.
*/
public static function dataProviderAlterQuery()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach (static::getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* Test the `applyOn` method.
*/
@@ -282,6 +244,22 @@ abstract class AbstractAggregatorTest extends KernelTestCase
);
}
/**
* prepare data for `testGetQueryKeys`.
*/
public static function dataProviderGetQueryKeys()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach ($datas as $data) {
yield [$data];
}
}
/**
* Test that.
*
@@ -346,6 +324,28 @@ abstract class AbstractAggregatorTest extends KernelTestCase
}
}
/**
* prepare date for method `testGetResultsAndLabels`.
*/
public static function dataProviderGetResultsAndLabels()
{
$datas = static::getFormData();
if (!\is_array($datas)) {
$datas = iterator_to_array($datas);
}
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* test the `getTitle` method.
*/

View File

@@ -31,27 +31,6 @@ abstract class AbstractExportTest extends WebTestCase
{
use PrepareClientTrait;
public static function dataProviderGetQueryKeys()
{
foreach (static::getFormData() as $data) {
yield [$data];
}
}
/**
* create data for `ìnitiateQuery` method.
*/
public static function dataProviderInitiateQuery()
{
$acl = static::getAcl();
foreach (static::getModifiersCombination() as $modifiers) {
foreach (static::getFormData() as $data) {
yield [$modifiers, $acl, $data];
}
}
}
/**
* Return an array usable as ACL.
*
@@ -198,6 +177,13 @@ abstract class AbstractExportTest extends WebTestCase
}
}
public static function dataProviderGetQueryKeys()
{
foreach (static::getFormData() as $data) {
yield [$data];
}
}
/**
* Test that.
*
@@ -382,4 +368,18 @@ abstract class AbstractExportTest extends WebTestCase
}
}
}
/**
* create data for `ìnitiateQuery` method.
*/
public static function dataProviderInitiateQuery()
{
$acl = static::getAcl();
foreach (static::getModifiersCombination() as $modifiers) {
foreach (static::getFormData() as $data) {
yield [$modifiers, $acl, $data];
}
}
}
}

View File

@@ -43,61 +43,6 @@ abstract class AbstractFilterTest extends KernelTestCase
self::ensureKernelShutdown();
}
/**
* provide data for `testAliasDidNotDisappears`.
*/
public static function dataProviderAliasDidNotDisappears()
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
public static function dataProviderAlterQuery()
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
public static function dataProvideQueryExecution(): iterable
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
public static function dataProviderDescriptionAction()
{
foreach (static::getFormData() as $data) {
yield [$data];
}
}
/**
* Create a filter which will be used in tests.
*
@@ -149,6 +94,24 @@ abstract class AbstractFilterTest extends KernelTestCase
}
}
/**
* provide data for `testAliasDidNotDisappears`.
*/
public static function dataProviderAliasDidNotDisappears()
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* test the alteration of query by the filter.
*
@@ -187,6 +150,21 @@ abstract class AbstractFilterTest extends KernelTestCase
);
}
public static function dataProviderAlterQuery()
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
/**
* @dataProvider dataProvideQueryExecution
*/
@@ -199,6 +177,21 @@ abstract class AbstractFilterTest extends KernelTestCase
self::assertIsArray($actual);
}
public static function dataProvideQueryExecution(): iterable
{
$datas = static::getFormData();
foreach (static::getQueryBuilders() as $qb) {
if ([] === $datas) {
yield [clone $qb, []];
} else {
foreach ($datas as $data) {
yield [clone $qb, $data];
}
}
}
}
public function testApplyOn()
{
$filter = $this->getFilter();
@@ -263,6 +256,13 @@ abstract class AbstractFilterTest extends KernelTestCase
}
}
public static function dataProviderDescriptionAction()
{
foreach (static::getFormData() as $data) {
yield [$data];
}
}
public function testGetTitle()
{
$title = $this->getFilter()->getTitle();

View File

@@ -32,6 +32,17 @@ final class AddressControllerTest extends \Symfony\Bundle\FrameworkBundle\Test\W
self::ensureKernelShutdown();
}
/**
* @dataProvider generateAddressIds
*/
public function testDuplicate(int $addressId)
{
$this->client = $this->getClientAuthenticated();
$this->client->request('POST', "/api/1.0/main/address/{$addressId}/duplicate.json");
$this->assertResponseIsSuccessful('test that duplicate is successful');
}
public static function generateAddressIds(): iterable
{
self::bootKernel();
@@ -49,15 +60,4 @@ final class AddressControllerTest extends \Symfony\Bundle\FrameworkBundle\Test\W
self::ensureKernelShutdown();
}
/**
* @dataProvider generateAddressIds
*/
public function testDuplicate(int $addressId)
{
$this->client = $this->getClientAuthenticated();
$this->client->request('POST', "/api/1.0/main/address/{$addressId}/duplicate.json");
$this->assertResponseIsSuccessful('test that duplicate is successful');
}
}

View File

@@ -25,6 +25,22 @@ final class AddressReferenceApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
/**
* @dataProvider provideData
*/
public function testSearch(int $postCodeId, string $pattern)
{
$client = $this->getClientAuthenticated();
$client->request(
'GET',
"/api/1.0/main/address-reference/by-postal-code/{$postCodeId}/search.json",
['q' => $pattern]
);
$this->assertResponseIsSuccessful();
}
public static function provideData()
{
self::bootKernel();
@@ -42,20 +58,4 @@ final class AddressReferenceApiControllerTest extends WebTestCase
yield [$postalCode->getId(), 'rue'];
}
/**
* @dataProvider provideData
*/
public function testSearch(int $postCodeId, string $pattern)
{
$client = $this->getClientAuthenticated();
$client->request(
'GET',
"/api/1.0/main/address-reference/by-postal-code/{$postCodeId}/search.json",
['q' => $pattern]
);
$this->assertResponseIsSuccessful();
}
}

View File

@@ -52,26 +52,6 @@ class AddressToReferenceMatcherControllerTest extends WebTestCase
$this->assertEquals(Address::ADDR_REFERENCE_STATUS_REVIEWED, $address->getRefStatus());
}
/**
* @dataProvider addressUnsyncedProvider
*/
public function testSyncAddressWithReference(int $addressId): void
{
$client = $this->getClientAuthenticated();
$client->request('POST', "/api/1.0/main/address/reference-match/{$addressId}/sync-with-reference");
$this->assertResponseIsSuccessful();
$this->addressRepository = self::getContainer()->get(AddressRepository::class);
$address = $this->addressRepository->find($addressId);
$this->assertEquals(Address::ADDR_REFERENCE_STATUS_MATCH, $address->getRefStatus());
$this->assertEquals($address->getAddressReference()->getStreet(), $address->getStreet());
$this->assertEquals($address->getAddressReference()->getStreetNumber(), $address->getStreetNumber());
$this->assertEquals($address->getAddressReference()->getPoint()->toWKT(), $address->getPoint()->toWKT());
}
public static function addressToReviewProvider(): iterable
{
self::bootKernel();
@@ -98,6 +78,26 @@ class AddressToReferenceMatcherControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
/**
* @dataProvider addressUnsyncedProvider
*/
public function testSyncAddressWithReference(int $addressId): void
{
$client = $this->getClientAuthenticated();
$client->request('POST', "/api/1.0/main/address/reference-match/{$addressId}/sync-with-reference");
$this->assertResponseIsSuccessful();
$this->addressRepository = self::getContainer()->get(AddressRepository::class);
$address = $this->addressRepository->find($addressId);
$this->assertEquals(Address::ADDR_REFERENCE_STATUS_MATCH, $address->getRefStatus());
$this->assertEquals($address->getAddressReference()->getStreet(), $address->getStreet());
$this->assertEquals($address->getAddressReference()->getStreetNumber(), $address->getStreetNumber());
$this->assertEquals($address->getAddressReference()->getPoint()->toWKT(), $address->getPoint()->toWKT());
}
public static function addressUnsyncedProvider(): iterable
{
self::bootKernel();

View File

@@ -55,6 +55,25 @@ class NewsItemControllerTest extends WebTestCase
$em->flush();
}
public function testList()
{
$client = $this->getClientAuthenticated('admin', 'password');
$client->request('GET', '/fr/admin/news_item');
self::assertResponseIsSuccessful('News item admin page shows');
}
/**
* @dataProvider generateNewsItemIds
*/
public function testShowSingleItem(NewsItem $newsItem)
{
$client = $this->getClientAuthenticated('admin', 'password');
$client->request('GET', "/fr/admin/news_item/{$newsItem->getId()}/view");
self::assertResponseIsSuccessful('Single news item admin page loads successfully');
}
public static function generateNewsItemIds(): iterable
{
self::bootKernel();
@@ -74,23 +93,4 @@ class NewsItemControllerTest extends WebTestCase
yield [$newsItem];
}
public function testList()
{
$client = $this->getClientAuthenticated('admin', 'password');
$client->request('GET', '/fr/admin/news_item');
self::assertResponseIsSuccessful('News item admin page shows');
}
/**
* @dataProvider generateNewsItemIds
*/
public function testShowSingleItem(NewsItem $newsItem)
{
$client = $this->getClientAuthenticated('admin', 'password');
$client->request('GET', "/fr/admin/news_item/{$newsItem->getId()}/view");
self::assertResponseIsSuccessful('Single news item admin page loads successfully');
}
}

View File

@@ -51,27 +51,6 @@ class NewsItemsHistoryControllerTest extends WebTestCase
self::ensureKernelShutdown();
}
public static function generateNewsItemIds(): iterable
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$news = new NewsItem();
$news->setContent('test content');
$news->setTitle('Title');
$news->setStartDate(new \DateTimeImmutable('yesterday'));
$em->persist($news);
$em->flush();
static::$toDelete[] = [NewsItem::class, $news];
self::ensureKernelShutdown();
yield [$news->getId()];
}
public function testList()
{
self::ensureKernelShutdown();
@@ -94,4 +73,25 @@ class NewsItemsHistoryControllerTest extends WebTestCase
$this->assertResponseIsSuccessful('test that single news item page loads successfully');
}
public static function generateNewsItemIds(): iterable
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$news = new NewsItem();
$news->setContent('test content');
$news->setTitle('Title');
$news->setStartDate(new \DateTimeImmutable('yesterday'));
$em->persist($news);
$em->flush();
static::$toDelete[] = [NewsItem::class, $news];
self::ensureKernelShutdown();
yield [$news->getId()];
}
}

View File

@@ -44,33 +44,6 @@ final class NotificationApiControllerTest extends WebTestCase
self::$toDelete = [];
}
public static function generateDataMarkAsRead()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$userRepository = self::getContainer()->get(UserRepository::class);
$userA = $userRepository->findOneBy(['username' => 'center a_social']);
$userB = $userRepository->findOneBy(['username' => 'center b_social']);
$notification = new Notification();
$notification
->setMessage('Test generated')
->setRelatedEntityClass(AccompanyingPeriod::class)
->setRelatedEntityId(0)
->setSender($userB)
->addAddressee($userA)
->setUpdatedAt(new \DateTimeImmutable());
$em->persist($notification);
$em->refresh($notification);
$em->flush();
self::$toDelete[] = [Notification::class, $notification->getId()];
self::ensureKernelShutdown();
yield [$notification->getId()];
}
/**
* @dataProvider generateDataMarkAsRead
*/
@@ -99,4 +72,31 @@ final class NotificationApiControllerTest extends WebTestCase
$this->assertFalse($notification->isReadBy($user));
}
public static function generateDataMarkAsRead()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$userRepository = self::getContainer()->get(UserRepository::class);
$userA = $userRepository->findOneBy(['username' => 'center a_social']);
$userB = $userRepository->findOneBy(['username' => 'center b_social']);
$notification = new Notification();
$notification
->setMessage('Test generated')
->setRelatedEntityClass(AccompanyingPeriod::class)
->setRelatedEntityId(0)
->setSender($userB)
->addAddressee($userA)
->setUpdatedAt(new \DateTimeImmutable());
$em->persist($notification);
$em->refresh($notification);
$em->flush();
self::$toDelete[] = [Notification::class, $notification->getId()];
self::ensureKernelShutdown();
yield [$notification->getId()];
}
}

View File

@@ -24,17 +24,6 @@ final class SearchApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
public static function generateSearchData()
{
yield ['per', ['person', 'thirdparty']];
yield ['per', ['thirdparty']];
yield ['per', ['person']];
yield ['fjklmeqjfkdqjklrmefdqjklm', ['person', 'thirdparty']];
}
/**
* @dataProvider generateSearchData
*/
@@ -50,4 +39,15 @@ final class SearchApiControllerTest extends WebTestCase
$this->assertResponseIsSuccessful();
}
public static function generateSearchData()
{
yield ['per', ['person', 'thirdparty']];
yield ['per', ['thirdparty']];
yield ['per', ['person']];
yield ['fjklmeqjfkdqjklrmefdqjklm', ['person', 'thirdparty']];
}
}

View File

@@ -27,25 +27,6 @@ final class UserControllerTest extends WebTestCase
{
use PrepareClientTrait;
public static function dataGenerateUserId()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
/** @var UserPasswordHasherInterface::class $passwordHasher */
$passwordHasher = self::getContainer()->get(UserPasswordHasherInterface::class);
$user = new User();
$user->setUsername('Test_user '.uniqid());
$user->setPassword($passwordHasher->hashPassword($user, 'password'));
$em->persist($user);
$em->flush();
self::ensureKernelShutdown();
yield [$user->getId(), $user->getUsername()];
}
public function testList()
{
$client = $this->getClientAuthenticatedAsAdmin();
@@ -135,6 +116,25 @@ final class UserControllerTest extends WebTestCase
$this->isPasswordValid($username, $newPassword);
}
public static function dataGenerateUserId()
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
/** @var UserPasswordHasherInterface::class $passwordHasher */
$passwordHasher = self::getContainer()->get(UserPasswordHasherInterface::class);
$user = new User();
$user->setUsername('Test_user '.uniqid());
$user->setPassword($passwordHasher->hashPassword($user, 'password'));
$em->persist($user);
$em->flush();
self::ensureKernelShutdown();
yield [$user->getId(), $user->getUsername()];
}
protected function isPasswordValid($username, $password)
{
/** @var \Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher $passwordEncoder */

View File

@@ -31,6 +31,22 @@ final class AgeTest extends KernelTestCase
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
}
/**
* @dataProvider generateQueries
*/
public function testWorking(string $dql, array $args)
{
$dql = $this->entityManager->createQuery($dql)->setMaxResults(3);
foreach ($args as $key => $value) {
$dql->setParameter($key, $value);
}
$results = $dql->getResult();
$this->assertIsArray($results);
}
public static function generateQueries(): iterable
{
yield [
@@ -60,20 +76,4 @@ final class AgeTest extends KernelTestCase
],
];
}
/**
* @dataProvider generateQueries
*/
public function testWorking(string $dql, array $args)
{
$dql = $this->entityManager->createQuery($dql)->setMaxResults(3);
foreach ($args as $key => $value) {
$dql->setParameter($key, $value);
}
$results = $dql->getResult();
$this->assertIsArray($results);
}
}

View File

@@ -31,13 +31,6 @@ final class JsonExtractTest extends KernelTestCase
$this->em = self::getContainer()->get(EntityManagerInterface::class);
}
public static function dataGenerateDql(): iterable
{
yield ['SELECT JSON_EXTRACT(c.name, \'fr\') FROM '.Country::class.' c', []];
yield ['SELECT JSON_EXTRACT(c.name, :lang) FROM '.Country::class.' c', ['lang' => 'fr']];
}
/**
* @dataProvider dataGenerateDql
*/
@@ -50,4 +43,11 @@ final class JsonExtractTest extends KernelTestCase
$this->assertIsArray($results, 'simply test that the query return a result');
}
public static function dataGenerateDql(): iterable
{
yield ['SELECT JSON_EXTRACT(c.name, \'fr\') FROM '.Country::class.' c', []];
yield ['SELECT JSON_EXTRACT(c.name, :lang) FROM '.Country::class.' c', ['lang' => 'fr']];
}
}

View File

@@ -44,26 +44,6 @@ final class NotificationTest extends KernelTestCase
$em->flush();
}
public static function generateNotificationData()
{
self::bootKernel();
$userRepository = self::getContainer()->get(UserRepository::class);
$senderId = $userRepository
->findOneBy(['username' => 'center b_social'])
->getId();
$addressesIds = [];
$addressesIds[] = $userRepository
->findOneBy(['username' => 'center b_direction'])
->getId();
yield [
$senderId,
$addressesIds,
];
}
public function testAddAddresseeStoreAnUread()
{
$notification = new Notification();
@@ -139,4 +119,24 @@ final class NotificationTest extends KernelTestCase
$this->assertContains($addresseeId, $unreadIds);
}
}
public static function generateNotificationData()
{
self::bootKernel();
$userRepository = self::getContainer()->get(UserRepository::class);
$senderId = $userRepository
->findOneBy(['username' => 'center b_social'])
->getId();
$addressesIds = [];
$addressesIds[] = $userRepository
->findOneBy(['username' => 'center b_direction'])
->getId();
yield [
$senderId,
$addressesIds,
];
}
}

Some files were not shown because too many files have changed in this diff Show More