diff --git a/CHANGELOG.md b/CHANGELOG.md index 969ae069c..49943e927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,63 @@ and this project adheres to ## Unreleased + +## Test releases + +### 2021-04-26 + +* [Datepickers] datepickers fixed when using keyboard to enter date (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/545) +* [social_action] Display 'agents traitants' in parcours resumé and social action list (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/568) +* [Person_search] Closed parcours shown within an accordeon that can be opened/closed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/574) + +### 2021-04-24 + +* [notification email on course designation] allow raw string in email content generation +* [Accompanying period work] list evaluations associated to a work by startDate, and then by id, from the most recent to older +* [Documents] Change wording 'créer' to 'enregistrer' (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/634) +* [Parcours]: The number of 'mes parcours' displayed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/572) +* [Hompage_widget]: Renaming of tabs and removal of social actions tab (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/570) +* [activity]: Ignore thirdparties when creating a social action via an activity (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/573) +* [parcours]: change wording of warning message and button when user is not associated to a household yet (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/590#note_918370943) +* [Accompanying period work evaluations] list documents associated to a work by creation date, and then by id, from the most recent to older +* [Course comment] add validationConstraint NotNull and NotBlank on comment content, to avoid sql error +* [Notifications] delay the sending of notificaiton to kernel.terminate +* [Notifications / Period user change] fix the sending of notification when user changes +* [Activity form] invert 'incoming' and 'receiving' in Activity form +* [Activity form] keep the same order for 'attendee' field in new and edit form +* [list with period] use "sameas" test operator to introduce requestor in list +* [notification email on course designation] allow raw string in email content generation +* [Accompanying period work] list evaluations associated to a work by startDate, and then by id, from the most recent to older + + +### 2021-04-13 + +* [person] household address: add a form for editing the validFrom date (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/541) +* [person] householdmemberseditor: fix composition type bug in select form (vuejs) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/543) +* [docgen] add more persons choices in docgen for course: amongst requestor (if person), resources of course (if person), and PersonResource (if person); +* [docgen] add a new context with a list of activities in course +* [docgen] add a comment in budget lines +* [notifications] allow to send a notification to an email address. The address receive an access link +* [adresses] add constraints in database to avoid errors later: postcode not null, and validfrom <= validto +* [accompanying work editor] add a label on document title input + +### 2021-04-07 + +* notification list: move action buttons outside of the toggle +* fix detecting of non-read notification +* filter users which are disabled in search user api +* order query for location and add pagination in list +* allow every person which has part for a workflow to see the workflow page +* able to see the workflow if the evaluation document has been deleted +* hardcode the list of supported mime types for edition with collabora +* list of accompanying course: allow to see the pinned comment in list_item + +### 2021-04-06 + * [main] notification toggle read: correct js syntax for compilation in production (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/548) * [parcours] Display of interlocuteurs changed to flex-table in parcours edit page to prevent cut-off of information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/535) * [activity] espace entre les boutons pour supprimer les documents -## Test releases ### continuous release in February and March diff --git a/composer.json b/composer.json index c96738df8..d0fc51c0e 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "knplabs/knp-time-bundle": "^1.12", "league/csv": "^9.7.1", "nyholm/psr7": "^1.4", - "ocramius/package-versions": "^1.10", + "ocramius/package-versions": "^1.10 || ^2", "odolbeau/phone-number-bundle": "^3.6", "phpoffice/phpspreadsheet": "^1.16", "ramsey/uuid-doctrine": "^1.7", @@ -33,6 +33,7 @@ "symfony/expression-language": "^4.4", "symfony/form": "^4.4", "symfony/framework-bundle": "^4.4", + "symfony/http-foundation": "^4.4", "symfony/intl": "^4.4", "symfony/mailer": "^5.4", "symfony/mime": "^5.4", @@ -103,7 +104,8 @@ "ergebnis/composer-normalize": true, "ocramius/package-versions": true, "phpro/grumphp": true, - "phpstan/extension-installer": true + "phpstan/extension-installer": true, + "roave/you-are-using-it-wrong": true }, "bin-dir": "bin", "optimize-autoloader": true, diff --git a/src/Bundle/ChillActivityBundle/EntityListener/ActivityEntityListener.php b/src/Bundle/ChillActivityBundle/EntityListener/ActivityEntityListener.php index ab370e89e..88adf9723 100644 --- a/src/Bundle/ChillActivityBundle/EntityListener/ActivityEntityListener.php +++ b/src/Bundle/ChillActivityBundle/EntityListener/ActivityEntityListener.php @@ -66,9 +66,6 @@ class ActivityEntityListener $newAction->addPerson($person); } - foreach ($associatedThirdparties as $thirdparty) { - $newAction->setHandlingThierparty($thirdparty); - } $this->em->persist($newAction); $this->em->flush(); } diff --git a/src/Bundle/ChillActivityBundle/Form/ActivityType.php b/src/Bundle/ChillActivityBundle/Form/ActivityType.php index 1e1daead5..6e75bde25 100644 --- a/src/Bundle/ChillActivityBundle/Form/ActivityType.php +++ b/src/Bundle/ChillActivityBundle/Form/ActivityType.php @@ -373,8 +373,8 @@ class ActivityType extends AbstractType 'label' => $activityType->getLabel('sentReceived'), 'required' => $activityType->isRequired('sentReceived'), 'choices' => [ - 'Sent' => Activity::SENTRECEIVED_SENT, 'Received' => Activity::SENTRECEIVED_RECEIVED, + 'Sent' => Activity::SENTRECEIVED_SENT, ], ]); } diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php index 48f750c26..d960a0c21 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php @@ -12,13 +12,20 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Repository; use Chill\ActivityBundle\Entity\Activity; +use Chill\ActivityBundle\Entity\ActivityPresence; +use Chill\ActivityBundle\Entity\ActivityType; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; +use Chill\MainBundle\Entity\Location; +use Chill\MainBundle\Entity\LocationType; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Query\ResultSetMappingBuilder; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Security; @@ -72,6 +79,86 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte ->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy); } + public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array + { + $rsm = new ResultSetMappingBuilder($this->em); + + $sql = ' + SELECT + a.id AS activity_id, + date, + CASE WHEN durationtime IS NOT NULL THEN (EXTRACT(EPOCH from durationtime) / 60)::int ELSE 0 END AS durationtimeminute, + attendee_id, + comment_comment, + emergency, + sentreceived, + CASE WHEN traveltime IS NOT NULL THEN (EXTRACT(EPOCH from traveltime) / 60)::int ELSE 0 END AS traveltimeminute, + t.id AS type_id, t.name as type_name, + p.id AS presence_id, p.name AS presence_name, + location.id AS location_id, location.address_id, location.name AS location_name, location.phonenumber1, location.phonenumber2, location.email, + location.locationtype_id, locationtype.title AS locationtype_title, + users.userids AS userids, + thirdparties.thirdpartyids, + persons.personids, + actions.socialactionids, + issues.socialissueids + + FROM activity a + LEFT JOIN chill_main_location location ON a.location_id = location.id + LEFT JOIN chill_main_location_type locationtype ON location.locationtype_id = locationtype.id + LEFT JOIN activitytpresence p ON a.attendee_id = p.id + LEFT JOIN activitytype t ON a.type_id = t.id + LEFT JOIN LATERAL (SELECT jsonb_agg(user_id) userids, activity_id FROM activity_user AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS users ON TRUE + LEFT JOIN LATERAL (SELECT jsonb_agg(thirdparty_id) thirdpartyids, activity_id FROM activity_thirdparty AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS thirdparties ON TRUE + LEFT JOIN LATERAL (SELECT jsonb_agg(person_id) personids, activity_id FROM activity_person AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS persons ON TRUE + LEFT JOIN LATERAL (SELECT jsonb_agg(socialaction_id) socialactionids, activity_id FROM chill_activity_activity_chill_person_socialaction AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS actions ON TRUE + LEFT JOIN LATERAL (SELECT jsonb_agg(socialissue_id) socialissueids, activity_id FROM chill_activity_activity_chill_person_socialissue AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS issues ON TRUE + + WHERE accompanyingperiod_id = ? + ORDER BY a.date DESC, a.id DESC + LIMIT ? + '; + + $rsm + ->addEntityResult(Activity::class, 'a') + ->addFieldResult('a', 'activity_id', 'id') + ->addFieldResult('a', 'date', 'date') + ->addFieldResult('a', 'comment', 'comment') + ->addFieldResult('a', 'sentreceived', 'sentReceived') + ->addFieldResult('a', 'emergency', 'emergency') + ->addJoinedEntityResult(Location::class, 'location', 'a', 'location') + ->addFieldResult('location', 'location_id', 'id') + ->addFieldResult('location', 'location_name', 'name') + ->addFieldResult('location', 'phonenumber1', 'phonenumber1') + ->addFieldResult('location', 'phonenumber2', 'phonenumber2') + ->addFieldResult('location', 'email', 'email') + ->addJoinedEntityResult(LocationType::class, 'locationType', 'location', 'locationType') + ->addFieldResult('locationType', 'locationtype_id', 'id') + ->addFieldResult('locationType', 'locationtype_title', 'title') + ->addJoinedEntityResult(ActivityType::class, 'activityType', 'a', 'activityType') + ->addFieldResult('activityType', 'type_id', 'id') + ->addFieldResult('activityType', 'type_name', 'name') + ->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee') + ->addFieldResult('activityPresence', 'presence_id', 'id') + ->addFieldResult('activityPresence', 'presence_name', 'name') + + // results which cannot be mapped into entity + ->addScalarResult('comment_comment', 'comment', Types::TEXT) + ->addScalarResult('userids', 'userIds', Types::JSON) + ->addScalarResult('thirdpartyids', 'thirdPartyIds', Types::JSON) + ->addScalarResult('personids', 'personIds', Types::JSON) + ->addScalarResult('socialactionids', 'socialActionIds', Types::JSON) + ->addScalarResult('socialissueids', 'socialIssueIds', Types::JSON) + ->addScalarResult('durationtimeminute', 'durationTimeMinute', Types::INTEGER) + ->addScalarResult('traveltimeminute', 'travelTimeMinute', Types::INTEGER); + + $nq = $this->em->createNativeQuery($sql, $rsm); + + $nq->setParameter(0, $period->getId())->setParameter(1, $limit); + + return $nq->getResult(AbstractQuery::HYDRATE_ARRAY); + } + /** * @param array $orderBy * diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php index 56fb112f9..1fc6d22b1 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php @@ -21,6 +21,15 @@ interface ActivityACLAwareRepositoryInterface */ public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array; + /** + * Return a list of activities, simplified as array (not object). + * + * The aim of this method is to get a long list of activities and keep performance. + * + * @return array an array of array, each item representing an activity + */ + public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array; + /** * @return Activity[]|array */ diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/SocialIssuesAcc.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/SocialIssuesAcc.vue index 51957b010..4691a860a 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/SocialIssuesAcc.vue +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/SocialIssuesAcc.vue @@ -54,19 +54,19 @@ {{ $t('activity.select_first_a_social_issue') }} - + - {{ $t('activity.social_action_list_empty') }} + {{ $t('activity.social_action_list_empty') }} @@ -111,7 +111,7 @@ export default { return this.$store.state.socialIssuesOther; }, socialActionsList() { - return this.$store.state.socialActionsList; + return this.$store.getters.socialActionsListSorted; }, socialActionsSelected() { return this.$store.state.activity.socialActions; diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js index f94381fc2..ca56a5dae 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js @@ -101,6 +101,9 @@ const store = createStore({ state.activity.activityType.thirdPartiesVisible !== 0) ); }, + socialActionsListSorted(state) { + return [ ...state.socialActionsList].sort((a, b) => a.ordering - b.ordering); + }, }, mutations: { // SocialIssueAcc @@ -131,7 +134,6 @@ const store = createStore({ state.socialActionsList = []; }, addActionInList(state, action) { - //console.log('add action list', action.id); state.socialActionsList.push(action); }, updateActionsSelected(state, actions) { diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/new.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/new.html.twig index a7751cd5a..8e078702c 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/new.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/new.html.twig @@ -85,15 +85,15 @@ {{ form_row(form.comment) }} {% endif %} +{%- if form.attendee is defined -%} + {{ form_row(form.attendee) }} +{% endif %} + {%- if form.documents is defined -%} {{ form_row(form.documents) }}
{% endif %} -{%- if form.attendee is defined -%} - {{ form_row(form.attendee) }} -{% endif %} - {# TODO .. status #} {{ form_end(form) }} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Button/wopi_edit_document.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Button/wopi_edit_document.html.twig index f25c06030..abab2436d 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/Button/wopi_edit_document.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Button/wopi_edit_document.html.twig @@ -1,7 +1,3 @@ - -{# Twig way - TODO: une route, un template avec un header CHILL et un iframe -#} {{ 'online_edit_document'|trans }} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig index 8fbc3fa5c..3963b0715 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig @@ -53,6 +53,11 @@
  • + {% if chill_document_is_editable(document.object) %} +
  • + {{ document.object|chill_document_edit_button }} +
  • + {% endif %} {% endif %} {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
  • @@ -75,6 +80,11 @@
  • + {% if chill_document_is_editable(document.object) %} +
  • + {{ document.object|chill_document_edit_button }} +
  • + {% endif %} {% endif %} {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
  • diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/new.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/new.html.twig index df947c933..3187714a6 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/new.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/new.html.twig @@ -47,7 +47,7 @@
  • - +
  • {{ form_end(form) }} diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php new file mode 100644 index 000000000..573d77e2c --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php @@ -0,0 +1,133 @@ +client = $client; + $this->tempUrlGenerator = $tempUrlGenerator; + } + + public function read(StoredObject $document): string + { + try { + $response = $this + ->client + ->request( + Request::METHOD_GET, + $this + ->tempUrlGenerator + ->generate( + Request::METHOD_GET, + $document->getFilename() + ) + ->url + ); + } catch (Throwable $e) { + throw StoredObjectManagerException::errorDuringHttpRequest($e); + } + + if ($response->getStatusCode() !== Response::HTTP_OK) { + throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode()); + } + + try { + $data = $response->getContent(); + } catch (Throwable $e) { + throw StoredObjectManagerException::unableToGetResponseContent($e); + } + + if (false === $this->hasKeysAndIv($document)) { + return $data; + } + + $clearData = openssl_decrypt( + $data, + self::ALGORITHM, + // TODO: Why using this library and not use base64_decode() ? + Base64Url::decode($document->getKeyInfos()['k']), + OPENSSL_RAW_DATA, + pack('C*', ...$document->getIv()) + ); + + if (false === $clearData) { + throw StoredObjectManagerException::unableToDecrypt(openssl_error_string()); + } + + return $clearData; + } + + public function write(StoredObject $document, string $clearContent): void + { + $encryptedContent = $this->hasKeysAndIv($document) + ? openssl_encrypt( + $clearContent, + self::ALGORITHM, + // TODO: Why using this library and not use base64_decode() ? + Base64Url::decode($document->getKeyInfos()['k']), + OPENSSL_RAW_DATA, + pack('C*', ...$document->getIv()) + ) + : $clearContent; + + try { + $response = $this + ->client + ->request( + Request::METHOD_PUT, + $this + ->tempUrlGenerator + ->generate( + Request::METHOD_PUT, + $document->getFilename() + ) + ->url, + [ + 'body' => $encryptedContent, + ] + ); + } catch (TransportExceptionInterface $exception) { + throw StoredObjectManagerException::errorDuringHttpRequest($exception); + } + + if ($response->getStatusCode() !== Response::HTTP_CREATED) { + throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode()); + } + } + + private function hasKeysAndIv(StoredObject $storedObject): bool + { + return ([] !== $storedObject->getKeyInfos()) && ([] !== $storedObject->getIv()); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php new file mode 100644 index 000000000..3cf67cb0c --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php @@ -0,0 +1,34 @@ +discovery->discoverMimeType($document->getType()); - - foreach ($mime_type as $item) { - if (array_key_exists('default', $item) && 'true' === $item['default']) { - return true; - } - } - - return false; + return in_array($document->getType(), self::SUPPORTED_MIMES, true); } public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string diff --git a/src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php new file mode 100644 index 000000000..589515a05 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php @@ -0,0 +1,183 @@ +setFilename('encrypted.txt') + ->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')]) + ->setIv(unpack('C*', 'abcdefghijklmnop')), + hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string + 'The quick brown fox jumps over the lazy dog', // clear + ]; + + // Non-encrypted object + yield [ + (new StoredObject())->setFilename('non-encrypted.txt'), // The StoredObject + 'The quick brown fox jumps over the lazy dog', // Encrypted + 'The quick brown fox jumps over the lazy dog', // Clear + ]; + + /* UNHAPPY SCENARIO */ + + // Encrypted object with issue during HTTP communication + yield [ + (new StoredObject()) + ->setFilename('error_during_http_request.txt') + ->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')]) + ->setIv(unpack('C*', 'abcdefghijklmnop')), + hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string + 'The quick brown fox jumps over the lazy dog', // clear + StoredObjectManagerException::class, + ]; + + // Encrypted object with issue during HTTP communication: Invalid status code + yield [ + (new StoredObject()) + ->setFilename('invalid_statuscode.txt') + ->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')]) + ->setIv(unpack('C*', 'abcdefghijklmnop')), + hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string + 'The quick brown fox jumps over the lazy dog', // clear + StoredObjectManagerException::class, + ]; + + // Erroneous encrypted: Unable to decrypt exception. + yield [ + (new StoredObject()) + ->setFilename('unable_to_decrypt.txt') + ->setKeyInfos(['k' => base64_encode('WRONG_PASS_PHRASE')]) + ->setIv(unpack('C*', 'abcdefghijklmnop')), + 'WRONG_ENCODED_VALUE', // Binary encoded string + 'The quick brown fox jumps over the lazy dog', // clear + StoredObjectManagerException::class, + ]; + } + + /** + * @dataProvider getDataProvider + */ + 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 getDataProvider + */ + public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null) + { + if (null !== $exceptionClass) { + $this->expectException($exceptionClass); + } + + $storedObjectManager = $this->getSubject($storedObject, $encodedContent); + + $storedObjectManager->write($storedObject, $clearContent); + + self::assertEquals($clearContent, $storedObjectManager->read($storedObject)); + } + + private function getHttpClient(string $encodedContent): HttpClientInterface + { + $callback = static function ($method, $url, $options) use ($encodedContent) { + if (Request::METHOD_GET === $method) { + switch ($url) { + case 'https://example.com/non-encrypted.txt': + case 'https://example.com/encrypted.txt': + return new MockResponse($encodedContent, ['http_code' => 200]); + + case 'https://example.com/error_during_http_request.txt': + return new TransportException('error_during_http_request.txt'); + + case 'https://example.com/invalid_statuscode.txt': + return new MockResponse($encodedContent, ['http_code' => 404]); + } + } + + if (Request::METHOD_PUT === $method) { + switch ($url) { + case 'https://example.com/non-encrypted.txt': + case 'https://example.com/encrypted.txt': + return new MockResponse($encodedContent, ['http_code' => 201]); + + case 'https://example.com/error_during_http_request.txt': + throw new TransportException('error_during_http_request.txt'); + + case 'https://example.com/invalid_statuscode.txt': + return new MockResponse($encodedContent, ['http_code' => 404]); + } + } + + return new MockResponse('Not found'); + }; + + return new MockHttpClient($callback); + } + + private function getSubject(StoredObject $storedObject, string $encodedContent): StoredObjectManagerInterface + { + return new StoredObjectManager( + $this->getHttpClient($encodedContent), + $this->getTempUrlGenerator($storedObject) + ); + } + + private function getTempUrlGenerator(StoredObject $storedObject): TempUrlGeneratorInterface + { + $response = new stdClass(); + $response->url = $storedObject->getFilename(); + + $tempUrlGenerator = $this + ->getMockBuilder(TempUrlGeneratorInterface::class) + ->getMock(); + + $tempUrlGenerator + ->method('generate') + ->withAnyParameters() + ->willReturn($response); + + return $tempUrlGenerator; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/composer.json b/src/Bundle/ChillDocStoreBundle/composer.json index c011ce29b..aa636535e 100644 --- a/src/Bundle/ChillDocStoreBundle/composer.json +++ b/src/Bundle/ChillDocStoreBundle/composer.json @@ -8,7 +8,8 @@ } }, "require": { - "symfony/mime": "^4 || ^5" + "symfony/mime": "^4 || ^5", + "symfony/http-foundation": "^4" }, "license": "AGPL-3.0" } diff --git a/src/Bundle/ChillDocStoreBundle/config/services.yaml b/src/Bundle/ChillDocStoreBundle/config/services.yaml index ff2818f6b..860495677 100644 --- a/src/Bundle/ChillDocStoreBundle/config/services.yaml +++ b/src/Bundle/ChillDocStoreBundle/config/services.yaml @@ -40,3 +40,8 @@ services: tags: - { name: 'serializer.normalizer', priority: 16 } + Chill\DocStoreBundle\Service\: + autowire: true + autoconfigure: true + resource: '../Service/' + diff --git a/src/Bundle/ChillMainBundle/Controller/LocationController.php b/src/Bundle/ChillMainBundle/Controller/LocationController.php index 10087ec1e..97deac9c5 100644 --- a/src/Bundle/ChillMainBundle/Controller/LocationController.php +++ b/src/Bundle/ChillMainBundle/Controller/LocationController.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; use Chill\MainBundle\CRUD\Controller\CRUDController; +use Chill\MainBundle\Pagination\PaginatorInterface; use Symfony\Component\HttpFoundation\Request; class LocationController extends CRUDController @@ -29,4 +30,9 @@ class LocationController extends CRUDController { $query->where('e.availableForUsers = true'); //TODO not working } + + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) + { + return $query->addOrderBy('e.name', 'DESC'); + } } diff --git a/src/Bundle/ChillMainBundle/Controller/NotificationController.php b/src/Bundle/ChillMainBundle/Controller/NotificationController.php index af9e1b2b1..d717f01eb 100644 --- a/src/Bundle/ChillMainBundle/Controller/NotificationController.php +++ b/src/Bundle/ChillMainBundle/Controller/NotificationController.php @@ -22,6 +22,7 @@ use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Repository\NotificationRepository; use Chill\MainBundle\Security\Authorization\NotificationVoter; use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -31,14 +32,19 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; +use function in_array; /** * @Route("/{_locale}/notification") */ class NotificationController extends AbstractController { + private LoggerInterface $chillLogger; + private EntityManagerInterface $em; + private LoggerInterface $logger; + private NotificationHandlerManager $notificationHandlerManager; private NotificationRepository $notificationRepository; @@ -51,6 +57,8 @@ class NotificationController extends AbstractController public function __construct( EntityManagerInterface $em, + LoggerInterface $chillLogger, + LoggerInterface $logger, Security $security, NotificationRepository $notificationRepository, NotificationHandlerManager $notificationHandlerManager, @@ -58,6 +66,8 @@ class NotificationController extends AbstractController TranslatorInterface $translator ) { $this->em = $em; + $this->logger = $logger; + $this->chillLogger = $chillLogger; $this->security = $security; $this->notificationRepository = $notificationRepository; $this->notificationHandlerManager = $notificationHandlerManager; @@ -150,6 +160,52 @@ class NotificationController extends AbstractController ]); } + /** + * @Route("/{id}/access_key", name="chill_main_notification_grant_access_by_access_key") + */ + public function getAccessByAccessKey(Notification $notification, Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + if (!$this->security->getUser() instanceof User) { + throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification'); + } + + foreach (['accessKey'/*, 'email'*/] as $param) { + if (!$request->query->has($param)) { + throw new BadRequestHttpException("Missing {$param} parameter"); + } + } + + if ($notification->getAccessKey() !== $request->query->getAlnum('accessKey')) { + throw new AccessDeniedHttpException('access key is invalid'); + } + + /* + desactivated due to escaped '&' in email links + if (!in_array($request->query->get('email'), $notification->getAddressesEmails(), true)) { + return (new Response('The email address is no more associated with this notification')) + ->setStatusCode(Response::HTTP_FORBIDDEN); + } + */ + + $notification->addAddressee($this->security->getUser()); + + $this->getDoctrine()->getManager()->flush(); + + $logMsg = '[Notification] a user is granted access to notification trough an access key'; + $context = [ + 'notificationId' => $notification->getId(), + 'email' => $request->query->get('email'), + 'user' => $this->security->getUser()->getId(), + ]; + + $this->logger->info($logMsg, $context); + $this->chillLogger->info($logMsg, $context); + + return $this->redirectToRoute('chill_main_notification_show', ['id' => $notification->getId()]); + } + /** * @Route("/inbox", name="chill_main_notification_my") */ diff --git a/src/Bundle/ChillMainBundle/Controller/SearchController.php b/src/Bundle/ChillMainBundle/Controller/SearchController.php index b9f5d19d8..41349336a 100644 --- a/src/Bundle/ChillMainBundle/Controller/SearchController.php +++ b/src/Bundle/ChillMainBundle/Controller/SearchController.php @@ -122,7 +122,7 @@ class SearchController extends AbstractController public function searchAction(Request $request, $_format) { - $pattern = $request->query->get('q', ''); + $pattern = trim($request->query->get('q', '')); if ('' === $pattern) { switch ($_format) { diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 8eac6668b..d3eee0b2a 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -140,6 +140,7 @@ class Address /** * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") + * @ORM\JoinColumn(nullable=false) * @Groups({"write"}) */ private ?PostalCode $postcode = null; @@ -480,8 +481,11 @@ class Address return $this; } - public function setStreet(string $street): self + public function setStreet(?string $street): self { + if (null === $street) { + $street = ''; + } $this->street = $street; return $this; @@ -515,8 +519,11 @@ class Address return $this; } - public function setStreetNumber(string $streetNumber): self + public function setStreetNumber(?string $streetNumber): self { + if (null === $streetNumber) { + $streetNumber = ''; + } $this->streetNumber = $streetNumber; return $this; diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index b2fe40c60..8dad39d12 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -18,6 +18,9 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use function count; +use function in_array; /** * @ORM\Entity @@ -31,15 +34,34 @@ use Symfony\Component\Validator\Constraints as Assert; */ class Notification implements TrackUpdateInterface { + /** + * @ORM\Column(type="text", nullable=false) + */ + private string $accessKey; + private array $addedAddresses = []; /** * @ORM\ManyToMany(targetEntity=User::class) * @ORM\JoinTable(name="chill_main_notification_addresses_user") - * @Assert\Count(min="1", minMessage="notification.At least one addressee") */ private Collection $addressees; + /** + * a list of destinee which will receive notifications. + * + * @var array|string[] + * @ORM\Column(type="json") + */ + private array $addressesEmails = []; + + /** + * a list of emails adresses which were added to the notification. + * + * @var array|string[] + */ + private array $addressesEmailsAdded = []; + private ?ArrayCollection $addressesOnLoad = null; /** @@ -111,6 +133,7 @@ class Notification implements TrackUpdateInterface $this->unreadBy = new ArrayCollection(); $this->comments = new ArrayCollection(); $this->setDate(new DateTimeImmutable()); + $this->accessKey = bin2hex(openssl_random_pseudo_bytes(24)); } public function addAddressee(User $addressee): self @@ -123,6 +146,14 @@ class Notification implements TrackUpdateInterface return $this; } + public function addAddressesEmail(string $email) + { + if (!in_array($email, $this->addressesEmails, true)) { + $this->addressesEmails[] = $email; + $this->addressesEmailsAdded[] = $email; + } + } + public function addComment(NotificationComment $comment): self { if (!$this->comments->contains($comment)) { @@ -142,6 +173,30 @@ class Notification implements TrackUpdateInterface return $this; } + /** + * @Assert\Callback + * + * @param array $payload + */ + public function assertCountAddresses(ExecutionContextInterface $context, $payload): void + { + if (0 === (count($this->getAddressesEmails()) + count($this->getAddressees()))) { + $context->buildViolation('notification.At least one addressee') + ->atPath('addressees') + ->addViolation(); + } + } + + public function getAccessKey(): string + { + return $this->accessKey; + } + + public function getAddedAddresses(): array + { + return $this->addedAddresses; + } + /** * @return Collection|User[] */ @@ -155,6 +210,22 @@ class Notification implements TrackUpdateInterface return $this->addressees; } + /** + * @return array|string[] + */ + public function getAddressesEmails(): array + { + return $this->addressesEmails; + } + + /** + * @return array|string[] + */ + public function getAddressesEmailsAdded(): array + { + return $this->addressesEmailsAdded; + } + public function getComments(): Collection { return $this->comments; @@ -271,6 +342,14 @@ class Notification implements TrackUpdateInterface return $this; } + public function removeAddressesEmail(string $email) + { + if (in_array($email, $this->addressesEmails, true)) { + $this->addressesEmails = array_filter($this->addressesEmails, static fn ($e) => $e !== $email); + $this->addressesEmailsAdded = array_filter($this->addressesEmailsAdded, static fn ($e) => $e !== $email); + } + } + public function removeComment(NotificationComment $comment): self { $this->comments->removeElement($comment); diff --git a/src/Bundle/ChillMainBundle/Form/NotificationType.php b/src/Bundle/ChillMainBundle/Form/NotificationType.php index b24513524..22fd19baf 100644 --- a/src/Bundle/ChillMainBundle/Form/NotificationType.php +++ b/src/Bundle/ChillMainBundle/Form/NotificationType.php @@ -12,12 +12,17 @@ declare(strict_types=1); namespace Chill\MainBundle\Form; use Chill\MainBundle\Entity\Notification; +use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\NotNull; class NotificationType extends AbstractType { @@ -30,9 +35,27 @@ class NotificationType extends AbstractType ]) ->add('addressees', PickUserDynamicType::class, [ 'multiple' => true, + 'required' => false, ]) ->add('message', ChillTextareaType::class, [ 'required' => false, + ]) + ->add('addressesEmails', ChillCollectionType::class, [ + 'label' => 'notification.dest by email', + 'help' => 'notification.dest by email help', + 'by_reference' => false, + 'allow_add' => true, + 'allow_delete' => true, + 'entry_type' => EmailType::class, + 'button_add_label' => 'notification.Add an email', + 'button_remove_label' => 'notification.Remove an email', + 'empty_collection_explain' => 'notification.Any email', + 'entry_options' => [ + 'constraints' => [ + new NotNull(), new NotBlank(), new Email(['checkMX' => true]), + ], + 'label' => 'Email', + ], ]); } diff --git a/src/Bundle/ChillMainBundle/Form/Type/AddressDateType.php b/src/Bundle/ChillMainBundle/Form/Type/AddressDateType.php new file mode 100644 index 000000000..299728d6e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/Type/AddressDateType.php @@ -0,0 +1,37 @@ +add( + 'validFrom', + ChillDateType::class, + [ + 'required' => true, + ] + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', Address::class); + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php index ce5587ed9..69fcf66f0 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -73,6 +73,17 @@ class NotificationMailer * Send a email after a notification is persisted. */ public function postPersistNotification(Notification $notification, LifecycleEventArgs $eventArgs): void + { + $this->sendNotificationEmailsToAddresses($notification); + $this->sendNotificationEmailsToAddressesEmails($notification); + } + + public function postUpdateNotification(Notification $notification, LifecycleEventArgs $eventArgs): void + { + $this->sendNotificationEmailsToAddressesEmails($notification); + } + + private function sendNotificationEmailsToAddresses(Notification $notification): void { foreach ($notification->getAddressees() as $addressee) { if (null === $addressee->getEmail()) { @@ -108,4 +119,31 @@ class NotificationMailer } } } + + private function sendNotificationEmailsToAddressesEmails(Notification $notification): void + { + foreach ($notification->getAddressesEmailsAdded() as $emailAddress) { + $email = new TemplatedEmail(); + $email + ->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig') + ->context([ + 'notification' => $notification, + 'dest' => $emailAddress, + ]); + + $email + ->subject($notification->getTitle()) + ->to($emailAddress); + + try { + $this->mailer->send($email); + } catch (TransportExceptionInterface $e) { + $this->logger->warning('[NotificationMailer] could not send an email notification', [ + 'to' => $emailAddress, + 'error_message' => $e->getMessage(), + 'error_trace' => $e->getTraceAsString(), + ]); + } + } + } } diff --git a/src/Bundle/ChillMainBundle/Notification/EventListener/PersistNotificationOnTerminateEventSubscriber.php b/src/Bundle/ChillMainBundle/Notification/EventListener/PersistNotificationOnTerminateEventSubscriber.php new file mode 100644 index 000000000..cb9d2db92 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/EventListener/PersistNotificationOnTerminateEventSubscriber.php @@ -0,0 +1,58 @@ +em = $em; + $this->persister = $persister; + } + + public static function getSubscribedEvents() + { + return [ + 'kernel.terminate' => [ + ['onKernelTerminate', 1024], // we must ensure that the priority is before sending email + ], + ]; + } + + public function onKernelTerminate(TerminateEvent $event): void + { + if ($event->isMasterRequest()) { + $this->persistNotifications(); + } + } + + private function persistNotifications(): void + { + if (0 < count($this->persister->getWaitingNotifications())) { + foreach ($this->persister->getWaitingNotifications() as $notification) { + $this->em->persist($notification); + } + + $this->em->flush(); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/NotificationPersister.php b/src/Bundle/ChillMainBundle/Notification/NotificationPersister.php new file mode 100644 index 000000000..d426e3cd7 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/NotificationPersister.php @@ -0,0 +1,32 @@ +waitingNotifications; + } + + public function persist(Notification $notification): void + { + $this->waitingNotifications[] = $notification; + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/NotificationPersisterInterface.php b/src/Bundle/ChillMainBundle/Notification/NotificationPersisterInterface.php new file mode 100644 index 000000000..da98e305b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/NotificationPersisterInterface.php @@ -0,0 +1,31 @@ +notificationByRelatedEntityAndUserAssociatedStatement) { $sql = 'SELECT - SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau JOIN chill_main_notification cmn ON cmnau.notification_id = cmn.id WHERE user_id = :userid and cmnau.notification_id = cmn.id and cmn.sender_id IS NOT NULL))::int) AS unread, + SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = :userid and cmnau.notification_id = cmn.id))::int) AS unread, SUM((cmn.sender_id = :userid)::int) AS sent, COUNT(cmn.*) AS total FROM chill_main_notification cmn diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss index 1750533f8..e4cdcb10c 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss @@ -16,6 +16,9 @@ // Chill forms @import './scss/forms'; +// Extend bootstrap accordion +@import './scss/accordion'; + // Chill record_actions @import './scss/record_actions'; diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/accordion.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/accordion.scss new file mode 100644 index 000000000..88d4b5e08 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/accordion.scss @@ -0,0 +1,11 @@ +.accordion { + // show a folded / label on accordion + button[aria-expanded="true"] > span.folded, + button[aria-expanded="false"] > span.unfolded { display: none; } + button[aria-expanded="false"] > span.folded, + button[aria-expanded="true"] > span.unfolded { display: inline; } + + .accordion-item { + margin-bottom: 1rem; + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js b/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js index 6308f2f11..ae092f8e2 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js +++ b/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js @@ -26,7 +26,7 @@ window.addEventListener('DOMContentLoaded', function (e) { buttonClass: el.dataset.buttonClass, buttonNoText: 'false' === el.dataset.buttonText, showUrl: el.dataset.showButtonUrl, - isRead: 1 === el.dataset.notificationCurrentIsRead, + isRead: 1 === Number.parseInt(el.dataset.notificationCurrentIsRead), container: el.dataset.container } }, diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue index 343b638da..fbf453b47 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue @@ -25,14 +25,14 @@ {{ $t('my_accompanying_courses.tab') }} - {% endmacro %} +{% macro accompanying_period(acp, person) %} + {% set app = person.findParticipationForPeriod(acp) %} +
    +
    +
    +
    +

    + + {{ 'Course number'|trans }} {{ acp.id }} +

    +
    +
    +
    + {% if app != null %} +
    + {{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }} +
    + {% endif %} + + {% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', acp.id) %} + {% if notif_counter.total > 0 %} + {{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', acp.id) }} + {% endif %} +
    +
    + {% if acp.requestoranonymous == false and acp.requestorPerson is same as(person) %} + + {{ 'Requestor'|trans({'gender': person.gender}) }} + + {% endif %} + + {% if acp.emergency %} + {{- 'Emergency'|trans|upper -}} + {% endif %} + + {% if acp.confidential %} + {{- 'Confidential'|trans|upper -}} + {% endif %} + + {% if acp.step == 'DRAFT' %} + {{ 'course.draft'|trans }} + {% endif %} + + {% if acp.step == 'CLOSED' %} + {{ 'course.closed'|trans }} + {% endif %} +
    +
    +
    + + {% if acp.user is not null %} +
    +
    +

    {{ 'Referrer'|trans }}

    +
    +
    +
    + {{ acp.user|chill_entity_render_box }} +
    +
    +
    + {% endif %} + + {% if acp.socialIssues|length > 0 %} +
    +
    +

    {{ 'Social issues'|trans }}

    +
    +
    + {% for issue in acp.socialIssues %} + {{ issue|chill_entity_render_box }} + {% endfor %} +
    +
    + {% endif %} + + {% if acp.currentParticipations|length > 1 %} +
    +
    +

    + {{ 'Participants'|trans }} +

    +
    +
    + {% set participating = false %} + {% for part in acp.currentParticipations %} + {% if part.person.id != person.id %} + {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { + targetEntity: { name: 'person', id: part.person.id }, + action: 'show', + displayBadge: true, + buttonText: part.person|chill_entity_render_string, + isDead: part.person.deathdate is not null + } %} + {% else %} + {% set participating = true %} + {% endif %} + {% endfor %} + {% if participating %} + {{ 'person.and_himself'|trans({'gender': person.gender}) }} + {% endif %} +
    +
    + {% endif %} + + {% if acp.requestoranonymous == false %} + {% if (acp.requestorPerson is not null and acp.requestorPerson.id != person.id) or acp.requestorThirdParty is not null %} +
    +
    +

    + {% if acp.requestorPerson is not null %} + {{ 'Requestor'|trans({'gender': acp.requestorPerson.gender}) }} + {% else %} + {{ 'Requestor'|trans({'gender': 'other'})}} + {% endif %} +

    +
    +
    + {% if acp.requestorThirdParty is not null %} + {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { + targetEntity: { name: 'thirdparty', id: acp.requestorThirdParty.id }, + action: 'show', + displayBadge: true, + buttonText: acp.requestorThirdParty|chill_entity_render_string + } %} + {% else %} + {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { + targetEntity: { name: 'person', id: acp.requestorPerson.id }, + action: 'show', + displayBadge: true, + buttonText: acp.requestorPerson|chill_entity_render_string, + isDead: acp.requestorPerson.deathdate is not null + } %} + {% endif %} +
    +
    + {% endif %} + {% endif %} + + +
    +
    +{% endmacro %} +

    {{ title|default('Person search results')|trans }}

    @@ -66,167 +217,53 @@ {#- 'acps' is for AcCompanyingPeriodS #} {%- set acps = [] %} + {%- set acpsClosed = [] %} {%- for acp in person.accompanyingPeriodInvolved %} {%- if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', acp) %} - {%- set acps = acps|merge([acp]) %} + {% if acp.step == 'CLOSED' %} + {%- set acpsClosed = acpsClosed|merge([acp]) %} + {% else %} + {%- set acps = acps|merge([acp]) %} + {% endif %} {%- endif %} {%- endfor %} {# add as requestor #} {% if acps|length > 0 %} {% for acp in acps %} - {% set app = person.findParticipationForPeriod(acp) %} -
    -
    -
    -
    -

    - - {{ 'Course number'|trans }} {{ acp.id }} -

    -
    -
    -
    - {% if app != null %} -
    - {{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }} -
    - {% endif %} - - {% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', acp.id) %} - {% if notif_counter.total > 0 %} - {{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', acp.id) }} - {% endif %} -
    -
    - {% if acp.requestoranonymous == false and acp.requestorPerson == person %} - - {{ 'Requestor'|trans({'gender': person.gender}) }} - - {% endif %} - - {% if acp.emergency %} - {{- 'Emergency'|trans|upper -}} - {% endif %} - - {% if acp.confidential %} - {{- 'Confidential'|trans|upper -}} - {% endif %} - - {% if acp.step == 'DRAFT' %} - {{ 'course.draft'|trans }} - {% endif %} - - {% if acp.step == 'CLOSED' %} - {{ 'course.closed'|trans }} - {% endif %} -
    -
    -
    - - {% if acp.user is not null %} -
    -
    -

    {{ 'Referrer'|trans }}

    -
    -
    -
    - {{ acp.user|chill_entity_render_box }} -
    -
    -
    - {% endif %} - - {% if acp.socialIssues|length > 0 %} -
    -
    -

    {{ 'Social issues'|trans }}

    -
    -
    - {% for issue in acp.socialIssues %} - {{ issue|chill_entity_render_box }} - {% endfor %} -
    -
    - {% endif %} - - {% if acp.currentParticipations|length > 1 %} -
    -
    -

    - {{ 'Participants'|trans }} -

    -
    -
    - {% set participating = false %} - {% for part in acp.currentParticipations %} - {% if part.person.id != person.id %} - {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { - targetEntity: { name: 'person', id: part.person.id }, - action: 'show', - displayBadge: true, - buttonText: part.person|chill_entity_render_string, - isDead: part.person.deathdate is not null - } %} - {% else %} - {% set participating = true %} - {% endif %} - {% endfor %} - {% if participating %} - {{ 'person.and_himself'|trans({'gender': person.gender}) }} - {% endif %} -
    -
    - {% endif %} - - {% if acp.requestoranonymous == false %} - {% if (acp.requestorPerson is not null and acp.requestorPerson.id != person.id) or acp.requestorThirdParty is not null %} -
    -
    -

    - {% if acp.requestorPerson is not null %} - {{ 'Requestor'|trans({'gender': acp.requestorPerson.gender}) }} - {% else %} - {{ 'Requestor'|trans({'gender': 'other'})}} - {% endif %} -

    -
    -
    - {% if acp.requestorThirdParty is not null %} - {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { - targetEntity: { name: 'thirdparty', id: acp.requestorThirdParty.id }, - action: 'show', - displayBadge: true, - buttonText: acp.requestorThirdParty|chill_entity_render_string - } %} - {% else %} - {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { - targetEntity: { name: 'person', id: acp.requestorPerson.id }, - action: 'show', - displayBadge: true, - buttonText: acp.requestorPerson|chill_entity_render_string, - isDead: acp.requestorPerson.deathdate is not null - } %} - {% endif %} -
    -
    - {% endif %} - {% endif %} - - - -
    -
    + {{ _self.accompanying_period(acp, person) }} {% endfor %} {% endif %} + {% if acpsClosed|length > 0 %} +
    +
    +

    + +

    + +
    + {% for acp in acpsClosed %} + {{ _self.accompanying_period(acp, person) }} + {% endfor %} +
    +
    +
    + {% endif %} + +
    {% endfor %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation_document.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation_document.html.twig index 2e05a6648..984277297 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation_document.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation_document.html.twig @@ -1,133 +1,139 @@ {%- import "@ChillDocStore/Macro/macro.html.twig" as m -%} -
    -
    -
    -

    {{ doc.title }}

    -
    -
    -

    - - - {{ evaluation.accompanyingPeriodWork.socialAction|chill_entity_render_string }} -
      -
    • - {{ 'accompanying_course_work.start_date'|trans ~ ' : ' }} - {{ evaluation.accompanyingPeriodWork.startDate|format_date('short') }} -
    • - {% if evaluation.accompanyingPeriodWork.endDate %} +{% if doc is null %} +
      + {{ 'workflow.doc for evaluation deleted'|trans }} +
      +{% else %} +
      +
      +
      +

      {{ doc.title }}

      +
      +
      +

      + + + {{ evaluation.accompanyingPeriodWork.socialAction|chill_entity_render_string }} +
      • - {{ 'accompanying_course_work.end_date'|trans ~ ' : ' }} - {{ evaluation.accompanyingPeriodWork.endDate|format_date('short') }} + {{ 'accompanying_course_work.start_date'|trans ~ ' : ' }} + {{ evaluation.accompanyingPeriodWork.startDate|format_date('short') }}
      • - {% endif %} -
      -
      -

      -
      -
      -
      -

      - {{ 'Participants'|trans }} -

      -
      -
      - {% for p in evaluation.accompanyingPeriodWork.persons %} - {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { - targetEntity: { name: 'person', id: p.id }, - action: 'show', - displayBadge: true, - buttonText: p|chill_entity_render_string, - isDead: p.deathdate is not null - } %} - {% endfor %} -
      -
      -
      - - - - - - - - - - - -
      -

      - {{ 'Évaluation'|trans }} -

      -
      -
        -
      • - {{ evaluation.evaluation.title|localize_translatable_string }} -
          -
        • - {{ 'accompanying_course_work.start_date'|trans ~ ' : ' }} - {{ evaluation.startDate|format_date('short') }} -
        • - {% if evaluation.endDate %} -
        • - {{ 'accompanying_course_work.end_date'|trans ~ ' : ' }} - {{ evaluation.endDate|format_date('short') }} -
        • - {% endif %} - {% if evaluation.maxDate %} -
        • - {{ 'accompanying_course_work.max_date'|trans ~ ' : ' }} - {{ evaluation.maxDate|format_date('short') }} -
        • - {% endif %} - {% if evaluation.warningInterval and evaluation.warningInterval.d > 0 %} -
        • - {% set days = (evaluation.warningInterval.d + evaluation.warningInterval.m * 30) %} - {{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }} - {{ 'accompanying_course_work.%days% days before max_date'|trans({'%days%': days }) }} -
        • - {% endif %} -
        • - {% if evaluation.createdBy is not null %} - créé par - {{ evaluation.createdBy.username }} - {% endif %} - {% if evaluation.createdAt is not null %} - {{ 'le'|trans }} - {{ evaluation.createdAt|format_date('short') }} - {% endif %} -
        • -
        - {% if evaluation.comment %} -
        - {{ evaluation.comment }} -
        - {% endif %} + {% if evaluation.accompanyingPeriodWork.endDate %} +
      • + {{ 'accompanying_course_work.end_date'|trans ~ ' : ' }} + {{ evaluation.accompanyingPeriodWork.endDate|format_date('short') }}
      • -
      -
      -
      -
      - {% import '@ChillPerson/Macro/updatedBy.html.twig' as macro %} - {{ macro.updatedBy(evaluation) }} + {% endif %} +
    +
    +

    +
    +
    +
    +

    + {{ 'Participants'|trans }} +

    +
    +
    + {% for p in evaluation.accompanyingPeriodWork.persons %} + {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { + targetEntity: { name: 'person', id: p.id }, + action: 'show', + displayBadge: true, + buttonText: p|chill_entity_render_string, + isDead: p.deathdate is not null + } %} + {% endfor %} +
    +
    +
    + + + + + + + + + + + +
    +

    + {{ 'Évaluation'|trans }} +

    +
    +
      +
    • + {{ evaluation.evaluation.title|localize_translatable_string }} +
        +
      • + {{ 'accompanying_course_work.start_date'|trans ~ ' : ' }} + {{ evaluation.startDate|format_date('short') }} +
      • + {% if evaluation.endDate %} +
      • + {{ 'accompanying_course_work.end_date'|trans ~ ' : ' }} + {{ evaluation.endDate|format_date('short') }} +
      • + {% endif %} + {% if evaluation.maxDate %} +
      • + {{ 'accompanying_course_work.max_date'|trans ~ ' : ' }} + {{ evaluation.maxDate|format_date('short') }} +
      • + {% endif %} + {% if evaluation.warningInterval and evaluation.warningInterval.d > 0 %} +
      • + {% set days = (evaluation.warningInterval.d + evaluation.warningInterval.m * 30) %} + {{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }} + {{ 'accompanying_course_work.%days% days before max_date'|trans({'%days%': days }) }} +
      • + {% endif %} +
      • + {% if evaluation.createdBy is not null %} + créé par + {{ evaluation.createdBy.username }} + {% endif %} + {% if evaluation.createdAt is not null %} + {{ 'le'|trans }} + {{ evaluation.createdAt|format_date('short') }} + {% endif %} +
      • +
      + {% if evaluation.comment %} +
      + {{ evaluation.comment }} +
      + {% endif %} +
    • +
    +
    +
    +
    + {% import '@ChillPerson/Macro/updatedBy.html.twig' as macro %} + {{ macro.updatedBy(evaluation) }} +
    - -{% if display_action is defined and display_action == true %} - {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', evaluation.accompanyingPeriodWork) %} - {% endif %} {% endif %} diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php index 490a7e2af..89f01bf94 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php @@ -23,6 +23,7 @@ use Chill\PersonBundle\Repository\Relationships\RelationshipRepository; use Chill\PersonBundle\Templating\Entity\PersonRenderInterface; use DateTimeInterface; use Doctrine\Common\Collections\ArrayCollection; +use libphonenumber\PhoneNumber; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; @@ -71,6 +72,7 @@ class PersonDocGenNormalizer implements $dateContext = $context; $dateContext['docgen:expects'] = DateTimeInterface::class; $addressContext = array_merge($context, ['docgen:expects' => Address::class]); + $phonenumberContext = array_merge($context, ['docgen:expects' => PhoneNumber::class]); $personResourceContext = array_merge($context, [ 'docgen:expects' => Person\PersonResource::class, // we simplify the list of attributes for the embedded persons @@ -113,9 +115,9 @@ class PersonDocGenNormalizer implements 'maritalStatus' => null !== ($ms = $person->getMaritalStatus()) ? $this->translatableStringHelper->localize($ms->getName()) : '', 'maritalStatusDate' => $this->normalizer->normalize($person->getMaritalStatusDate(), $format, $dateContext), 'email' => $person->getEmail(), - 'firstPhoneNumber' => $this->normalizer->normalize($person->getPhonenumber() ?? $person->getMobilenumber(), $format, $context), - 'fixPhoneNumber' => $this->normalizer->normalize($person->getPhonenumber(), $format, $context), - 'mobilePhoneNumber' => $this->normalizer->normalize($person->getMobilenumber(), $format, $context), + 'firstPhoneNumber' => $this->normalizer->normalize($person->getPhonenumber() ?? $person->getMobilenumber(), $format, $phonenumberContext), + 'fixPhoneNumber' => $this->normalizer->normalize($person->getPhonenumber(), $format, $phonenumberContext), + 'mobilePhoneNumber' => $this->normalizer->normalize($person->getMobilenumber(), $format, $phonenumberContext), 'nationality' => null !== ($c = $person->getNationality()) ? $this->translatableStringHelper->localize($c->getName()) : '', 'placeOfBirth' => $person->getPlaceOfBirth(), 'memo' => $person->getMemo(), diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/SocialActionNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/SocialActionNormalizer.php index e210e1c4f..52ba07a93 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/SocialActionNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/SocialActionNormalizer.php @@ -40,6 +40,7 @@ class SocialActionNormalizer implements NormalizerAwareInterface, NormalizerInte 'desactivationDate' => $this->normalizer->normalize($socialAction->getDesactivationDate(), $format, $context), 'title' => $socialAction->getTitle(), 'issue' => $this->normalizer->normalize($socialAction->getIssue(), $format, $context), + 'ordering' => $socialAction->getOrdering(), ]; case 'docgen': diff --git a/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php b/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php index 495989b29..dc6e9c912 100644 --- a/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php +++ b/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php @@ -152,8 +152,25 @@ class AccompanyingPeriodContext implements $options = $template->getOptions(); $persons = $entity->getCurrentParticipations()->map(static function (AccompanyingPeriodParticipation $p) { return $p->getPerson(); - }) - ->toArray(); + }); + + foreach ($entity->getCurrentParticipations() as $p) { + foreach ($p->getPerson()->getResources() as $r) { + if (null !== $r->getPerson() && !$persons->contains($r->getPerson())) { + $persons->add($r->getPerson()); + } + } + } + + if (null !== $entity->getRequestorPerson() && !$persons->contains($entity->getRequestorPerson())) { + $persons->add($entity->getRequestorPerson()); + } + + foreach ($entity->getResources() as $r) { + if (null !== $r->getPerson() && !$persons->contains($r->getPerson())) { + $persons->add($r->getPerson()); + } + } foreach (['mainPerson', 'person1', 'person2'] as $key) { if ($options[$key] ?? false) { diff --git a/src/Bundle/ChillPersonBundle/Tests/AccompanyingPeriod/Events/PersonMoveEventSubscriberTest.php b/src/Bundle/ChillPersonBundle/Tests/AccompanyingPeriod/Events/PersonMoveEventSubscriberTest.php index 321d69aac..892bd29f5 100644 --- a/src/Bundle/ChillPersonBundle/Tests/AccompanyingPeriod/Events/PersonMoveEventSubscriberTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/AccompanyingPeriod/Events/PersonMoveEventSubscriberTest.php @@ -14,6 +14,8 @@ namespace AccompanyingPeriod\Events; use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Notification\NotificationPersister; +use Chill\MainBundle\Notification\NotificationPersisterInterface; use Chill\PersonBundle\AccompanyingPeriod\Events\PersonAddressMoveEventSubscriber; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Household\Household; @@ -22,7 +24,6 @@ use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent; use DateTime; use DateTimeImmutable; -use Doctrine\ORM\EntityManagerInterface; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use ReflectionClass; @@ -73,9 +74,9 @@ final class PersonMoveEventSubscriberTest extends KernelTestCase ->setPreviousMembership($previousMembership) ->setNextMembership($nextMembership); - $em = $this->prophesize(EntityManagerInterface::class); - $em->persist(Argument::type(Notification::class))->shouldNotBeCalled(); - $eventSubscriber = $this->buildSubscriber(null, $em->reveal(), null, null); + $notificationPersister = $this->prophesize(NotificationPersisterInterface::class); + $notificationPersister->persist(Argument::type(Notification::class))->shouldNotBeCalled(); + $eventSubscriber = $this->buildSubscriber(null, $notificationPersister->reveal(), null, null); $eventSubscriber->resetPeriodLocation($event); } @@ -115,9 +116,9 @@ final class PersonMoveEventSubscriberTest extends KernelTestCase ->setPreviousMembership($previousMembership) ->setNextMembership($nextMembership); - $em = $this->prophesize(EntityManagerInterface::class); - $em->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1); - $eventSubscriber = $this->buildSubscriber(null, $em->reveal(), null, null); + $notificationPersister = $this->prophesize(NotificationPersisterInterface::class); + $notificationPersister->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1); + $eventSubscriber = $this->buildSubscriber(null, $notificationPersister->reveal(), null, null); $eventSubscriber->resetPeriodLocation($event); @@ -160,9 +161,9 @@ final class PersonMoveEventSubscriberTest extends KernelTestCase ->setPreviousMembership($previousMembership) ->setNextMembership($nextMembership); - $em = $this->prophesize(EntityManagerInterface::class); - $em->persist(Argument::type(Notification::class))->shouldBeCalled(1); - $eventSubscriber = $this->buildSubscriber(null, $em->reveal(), null, null); + $notificationPersister = $this->prophesize(NotificationPersisterInterface::class); + $notificationPersister->persist(Argument::type(Notification::class))->shouldBeCalled(1); + $eventSubscriber = $this->buildSubscriber(null, $notificationPersister->reveal(), null, null); $eventSubscriber->resetPeriodLocation($event); @@ -195,9 +196,9 @@ final class PersonMoveEventSubscriberTest extends KernelTestCase $event ->setPreviousMembership($previousMembership); - $em = $this->prophesize(EntityManagerInterface::class); - $em->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1); - $eventSubscriber = $this->buildSubscriber(null, $em->reveal(), null, null); + $notificationPersister = $this->prophesize(NotificationPersisterInterface::class); + $notificationPersister->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1); + $eventSubscriber = $this->buildSubscriber(null, $notificationPersister->reveal(), null, null); $eventSubscriber->resetPeriodLocation($event); @@ -235,9 +236,9 @@ final class PersonMoveEventSubscriberTest extends KernelTestCase ->setPreviousAddress($household->getPreviousAddressOf($newAddress)) ->setNextAddress($newAddress); - $em = $this->prophesize(EntityManagerInterface::class); - $em->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1); - $eventSubscriber = $this->buildSubscriber(null, $em->reveal(), null, null); + $notificationPersister = $this->prophesize(NotificationPersisterInterface::class); + $notificationPersister->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1); + $eventSubscriber = $this->buildSubscriber(null, $notificationPersister->reveal(), null, null); $eventSubscriber->resetPeriodLocation($event); @@ -247,7 +248,7 @@ final class PersonMoveEventSubscriberTest extends KernelTestCase private function buildSubscriber( ?EngineInterface $engine = null, - ?EntityManagerInterface $entityManager = null, + ?NotificationPersisterInterface $notificationPersister = null, ?Security $security = null, ?TranslatorInterface $translator = null ): PersonAddressMoveEventSubscriber { @@ -267,14 +268,13 @@ final class PersonMoveEventSubscriberTest extends KernelTestCase $engine = $double->reveal(); } - if (null === $entityManager) { - $double = $this->prophesize(EntityManagerInterface::class); - $entityManager = $double->reveal(); + if (null === $notificationPersister) { + $notificationPersister = new NotificationPersister(); } return new PersonAddressMoveEventSubscriber( $engine, - $entityManager, + $notificationPersister, $security, $translator ); diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php index 0b66d8648..542144bc2 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Tests\Entity; use ArrayIterator; use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; @@ -62,6 +63,29 @@ final class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase $this->assertFalse($period->isClosingAfterOpening()); } + public function testHasChangedUser() + { + $period = new AccompanyingPeriod(); + + $this->assertFalse($period->isChangedUser()); + $this->assertFalse($period->hasPreviousUser()); + + $period->setUser($user1 = new User()); + + $this->assertTrue($period->isChangedUser()); + $this->assertFalse($period->hasPreviousUser()); + + $period->resetPreviousUser(); + $this->assertFalse($period->isChangedUser()); + $this->assertFalse($period->hasPreviousUser()); + + $period->setUser($user2 = new User()); + + $this->assertTrue($period->isChangedUser()); + $this->assertTrue($period->hasPreviousUser()); + $this->assertSame($user1, $period->getPreviousUser()); + } + public function testHistoryLocation() { $period = new AccompanyingPeriod(); diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index d7ce1ed4b..578d8e195 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php @@ -48,6 +48,12 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW { $doc = $this->getRelatedEntity($entityWorkflow); + if (null === $doc) { + return [ + 'persons' => [], + ]; + } + return [ 'persons' => $doc->getAccompanyingPeriodWorkEvaluation() ->getAccompanyingPeriodWork()->getPersons(), @@ -58,6 +64,10 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW { $doc = $this->getRelatedEntity($entityWorkflow); + if (null === $doc) { + return $this->translator->trans('workflow.doc for evaluation deleted'); + } + return $this->translator->trans( 'workflow.Doc for evaluation (n°%eval%)', ['%eval%' => $entityWorkflow->getRelatedEntityId()] @@ -98,7 +108,7 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW return [ 'entity_workflow' => $entityWorkflow, - 'evaluation' => $doc->getAccompanyingPeriodWorkEvaluation(), + 'evaluation' => null !== $doc ? $doc->getAccompanyingPeriodWorkEvaluation() : $doc, 'doc' => $doc, ]; } diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml index 44f69b4c2..0e825aa72 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml @@ -114,3 +114,19 @@ household_composition: few {# enfants dans le ménage} other {# enfants dans le ménage} } + +periods: + show closed periods: >- + {nb_items, plural, + =0 {Aucun parcours clôturé} + one {Montrer un parcours clôturé} + many {Montrer # parcours clôturés} + other {Montrer # parcours clôturés} + } + hide closed periods: >- + {nb_items, plural, + =0 {Aucun parcours clôturé} + one {Masquer un parcours clôturé} + many {Masquer # parcours clôturés} + other {Masquer # parcours clôturés} + } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index ae7687324..77ff2bdb2 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -214,7 +214,7 @@ No requestor: Pas de demandeur No resources: "Pas d'interlocuteurs privilégiés" Persons associated: Usagers concernés Referrer: Référent -Referrers: Référents +Referrers: Agents traitants Some peoples does not belong to any household currently. Add them to an household soon: Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur ménage dès que possible. Add to household now: Ajouter à un ménage Any resource for this accompanying course: Aucun interlocuteur privilégié pour ce parcours @@ -465,6 +465,9 @@ fix it: Compléter accompanying_course: administrative_location: Localisation administrative comment is pinned: Le commentaire est épinglé +show: Montrer +hide: Masquer +closed periods: parcours clôturer # Accompanying Course comments Accompanying Course Comment: Commentaire @@ -498,6 +501,9 @@ Concerns household n°%id%: Concerne le ménage n°%id% Composition: Composition Budget: Budget The composition has been successfully removed.: La composition a été supprimée. +edit address valid from: Modifier la date du déménagement +Edit household address valid from: Modifier la date du déménagement + # accompanying course work Accompanying Course Actions: Actions d'accompagnements @@ -578,9 +584,11 @@ Linked evaluations: Évaluations associées # Accompanying period per user My accompanying periods: Mes parcours My accompanying periods in draft: Mes parcours brouillons +Number of periods: Nombre de parcours workflow: Doc for evaluation (n°%eval%): Document de l'évaluation n°%eval% + doc for evaluation deleted: Document supprimé dans une évaluation period_by_user_list: Period by user: Parcours d'accompagnement par utilisateur diff --git a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml index 492f50396..13fcb54fb 100644 --- a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml @@ -57,4 +57,8 @@ Only the referrer can change the confidentiality of a parcours: 'Seul le référ # resource You must associate at least one entity: Associez un usager, un tiers ou indiquez une description libre -You cannot associate a resource with the same person: Vous ne pouvez pas ajouter la personne elle-même en tant que ressource. \ No newline at end of file +You cannot associate a resource with the same person: Vous ne pouvez pas ajouter la personne elle-même en tant que ressource. + +#location +The period must remain located: 'Un parcours doit être localisé' +The person where the course is located must be associated to the course. Change course's location before removing the person.: "Le parcours est localisé auprès cet usager. Veuillez changer la localisation du parcours avant de suprimer l'usager" diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_edit.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_edit.html.twig index d0a906c9c..d7f923a46 100644 --- a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_edit.html.twig +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_edit.html.twig @@ -16,7 +16,7 @@