diff --git a/package.json b/package.json index 6b494a68d..17c7064a7 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,13 @@ "@fullcalendar/vue3": "^6.1.4", "@popperjs/core": "^2.9.2", "@types/leaflet": "^1.9.3", + "@types/dompurify": "^3.0.5", "dropzone": "^5.7.6", "es6-promise": "^4.2.8", "leaflet": "^1.7.1", + "marked": "^12.0.2", "masonry-layout": "^4.2.2", - "mime": "^3.0.0", + "mime": "^4.0.0", "swagger-ui": "^4.15.5", "vis-network": "^9.1.0", "vue": "^3.2.37", diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig index b3382c3de..13dfa7461 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig @@ -68,7 +68,7 @@

{{ 'Referrer'|trans }}

- {{ activity.user|chill_entity_render_box }} + {{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}

diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig index 48eb839d7..76db92d42 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig @@ -87,7 +87,8 @@
  • {% if bloc.type == 'user' %} - {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }} + hello + {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} {% else %} {{ _self.insert_onthefly(bloc.type, item) }} @@ -114,7 +115,7 @@
  • {% if bloc.type == 'user' %} - {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }} + {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} {% else %} {{ _self.insert_onthefly(bloc.type, item) }} @@ -142,7 +143,7 @@ {% if bloc.type == 'user' %} - {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }} + {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} {%- if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %} {% set invite = entity.inviteForUser(item) %} {% if invite is not null %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig index aa0bbea0c..04ea936d4 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig @@ -41,7 +41,7 @@ {% if activity.user and t.userVisible %}
  • {{ 'Referrer'|trans ~ ': ' }} - {{ activity.user|chill_entity_render_box }} + {{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}
  • {% endif %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig index d4beb606a..0330377aa 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig @@ -37,7 +37,7 @@ {%- if entity.user is not null %}
    {{ 'Referrer'|trans|capitalize }}
    - {{ entity.user|chill_entity_render_box }} + {{ entity.user|chill_entity_render_box({'at_date': entity.date}) }}
    {% endif %} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/index.html.twig b/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/index.html.twig index 1e2711bfe..0a8648749 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/index.html.twig +++ b/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/index.html.twig @@ -49,13 +49,13 @@
  • {{ 'By'|trans }}: - {{ entity.createdBy|chill_entity_render_box }} + {{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}
  • {{ 'For'|trans }}: - {{ entity.agent|chill_entity_render_box }} + {{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}
  • diff --git a/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/view.html.twig b/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/view.html.twig index 8fb487d31..4ef237336 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/view.html.twig +++ b/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/view.html.twig @@ -18,11 +18,11 @@
    {{ entity.type|chill_entity_render_box }}
    {{ 'Created by'|trans }}
    -
    {{ entity.createdBy }}
    +
    {{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}
    {{ 'Created for'|trans }}
    -
    {{ entity.agent }}
    - +
    {{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}
    +
    {{ 'Asideactivity location'|trans }}
    {%- if entity.location.name is defined -%}
    {{ entity.location.name }}
    diff --git a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php index 32308a50c..5962ae4d1 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php @@ -524,6 +524,16 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente return $this->startDate; } + /** + * get the date of the calendar. + * + * Useful for showing the date of the calendar event, required by twig in some places. + */ + public function getDate(): ?\DateTimeImmutable + { + return $this->getStartDate(); + } + public function getStatus(): ?string { return $this->status; diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/App.vue b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/App.vue index e09175a4b..a34ef7859 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/App.vue +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/App.vue @@ -16,6 +16,7 @@ :removableIfSet="false" :displayPicked="false" :suggested="this.suggestedUsers" + :label="'Utilisateur principal'" @addNewEntity="setMainUser" > diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig index 9e87af8ef..057139bd3 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig @@ -55,7 +55,7 @@
    @@ -132,7 +132,7 @@
  • {{ 'Created by'|trans }} - {{ calendar.activity.createdBy|chill_entity_render_string }}, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }} + {{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
  • {% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %} diff --git a/src/Bundle/ChillDocGeneratorBundle/Test/DocGenNormalizerTestAbstract.php b/src/Bundle/ChillDocGeneratorBundle/Test/DocGenNormalizerTestAbstract.php new file mode 100644 index 000000000..2839be3f5 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Test/DocGenNormalizerTestAbstract.php @@ -0,0 +1,65 @@ +getNormalizer()->normalize($this->provideNotNullObject(), 'docgen', [ + AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(), + ]); + $nullNormalizedObject = $this->getNormalizer()->normalize(null, 'docgen', [ + AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(), + ]); + + self::assertEqualsCanonicalizing(array_keys($normalizedObject), array_keys($nullNormalizedObject)); + self::assertArrayHasKey('isNull', $nullNormalizedObject, 'each object must have an "isNull" key'); + self::assertTrue($nullNormalizedObject['isNull'], 'isNull key must be true for null objects'); + self::assertFalse($normalizedObject['isNull'], 'isNull key must be false for null objects'); + + foreach ($normalizedObject as $key => $value) { + if (in_array($key, ['isNull', 'type'])) { + continue; + } + + if (is_array($value)) { + if (array_is_list($value)) { + self::assertEquals([], $nullNormalizedObject[$key], "list must be serialized as an empty array, in {$key}"); + } else { + self::assertEqualsCanonicalizing(array_keys($value), array_keys($nullNormalizedObject[$key]), "sub-object must have the same keys, in {$key}"); + } + } elseif (is_string($value)) { + self::assertEquals('', $nullNormalizedObject[$key], 'strings must be '); + } + } + } + + /** + * @return T + */ + abstract public function provideNotNullObject(): object; + + /** + * @return class-string + */ + abstract public function provideDocGenExpectClass(): string; + + abstract public function getNormalizer(): NormalizerInterface; +} diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index 05112430c..b94d90760 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -274,13 +274,13 @@ class User implements UserInterface, \Stringable return $this->mainLocation; } - public function getMainScope(?\DateTimeImmutable $at = null): ?Scope + public function getMainScope(?\DateTimeImmutable $atDate = null): ?Scope { - $at ??= new \DateTimeImmutable('now'); + $atDate ??= new \DateTimeImmutable('now'); foreach ($this->scopeHistories as $scopeHistory) { - if ($at >= $scopeHistory->getStartDate() && ( - null === $scopeHistory->getEndDate() || $at < $scopeHistory->getEndDate() + if ($atDate >= $scopeHistory->getStartDate() && ( + null === $scopeHistory->getEndDate() || $atDate < $scopeHistory->getEndDate() )) { return $scopeHistory->getScope(); } @@ -326,13 +326,13 @@ class User implements UserInterface, \Stringable return $this->salt; } - public function getUserJob(?\DateTimeImmutable $at = null): ?UserJob + public function getUserJob(?\DateTimeImmutable $atDate = null): ?UserJob { - $at ??= new \DateTimeImmutable('now'); + $atDate ??= new \DateTimeImmutable('now'); foreach ($this->jobHistories as $jobHistory) { - if ($at >= $jobHistory->getStartDate() && ( - null === $jobHistory->getEndDate() || $at < $jobHistory->getEndDate() + if ($atDate >= $jobHistory->getStartDate() && ( + null === $jobHistory->getEndDate() || $atDate < $jobHistory->getEndDate() )) { return $jobHistory->getJob(); } @@ -346,6 +346,11 @@ class User implements UserInterface, \Stringable return $this->jobHistories; } + public function getUserScopeHistories(): Collection + { + return $this->scopeHistories; + } + /** * @return ArrayCollection|UserJobHistory[] */ diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/download-report/download-report.js b/src/Bundle/ChillMainBundle/Resources/public/lib/download-report/download-report.js index 041e94c45..e7dce2d48 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/download-report/download-report.js +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/download-report/download-report.js @@ -43,7 +43,14 @@ var download_report = (url, container) => { content = URL.createObjectURL(blob); } - extension = mime.getExtension(type); + const extensions = new Map(); + extensions.set('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx'); + extensions.set('application/vnd.oasis.opendocument.spreadsheet', 'ods'); + extensions.set('application/vnd.ms-excel', 'xlsx'); + extensions.set('text/csv', 'csv'); + extensions.set('text/csv; charset=utf-8', 'csv'); + + extension = extensions.get(type); link.appendChild(document.createTextNode(download_text)); link.classList.add("btn", "btn-action"); @@ -55,7 +62,7 @@ var download_report = (url, container) => { container.innerHTML = ""; container.appendChild(link); }).catch(function(error) { - console.log(error); + console.error(error); var problem_text = document.createTextNode("Problem during download"); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue index bbad4315c..d3b499cac 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue @@ -139,8 +139,8 @@ const postprocess = (html: string): string => { } const convertMarkdownToHtml = (markdown: string): string => { - marked.use({'hooks': {postprocess, preprocess}}); - const rawHtml = marked(markdown); + marked.use({'hooks': {postprocess, preprocess}, 'async': false}); + const rawHtml = marked(markdown) as string; return rawHtml; }; diff --git a/src/Bundle/ChillMainBundle/Resources/views/Entity/CommentEmbeddable.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Entity/CommentEmbeddable.html.twig index 1dcd9cbf7..5395df212 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Entity/CommentEmbeddable.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Entity/CommentEmbeddable.html.twig @@ -40,10 +40,10 @@ {{ 'by_user'|trans ~ ' ' }} {% endif %} - {{ user|chill_entity_render_box(options['user']) }} + {{ user|chill_entity_render_box({'at_date': comment.date}) }} {% endif %} {% endif %} -{{ closing_box|raw }} \ No newline at end of file +{{ closing_box|raw }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig index c95308610..84973a096 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig @@ -1,10 +1,10 @@ {{- user.label }} - {%- if opts['user_job'] and user.userJob(opts['at']) is not null %} - ({{ user.userJob(opts['at']).label|localize_translatable_string }}) + {%- if opts['user_job'] and user.userJob(opts['at_date']) is not null %} + ({{ user.userJob(opts['at_date']).label|localize_translatable_string }}) {%- endif -%} - {%- if opts['main_scope'] and user.mainScope(opts['at']) is not null %} - ({{ user.mainScope(opts['at']).name|localize_translatable_string }}) + {%- if opts['main_scope'] and user.mainScope(opts['at_date']) is not null %} + ({{ user.mainScope(opts['at_date']).name|localize_translatable_string }}) {%- endif -%} {%- if opts['absence'] and user.isAbsent %} {{ 'absence.A'|trans }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig index 9bec09e4c..377c693ff 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig @@ -21,7 +21,7 @@ {% if not c.notification.isSystem %} - {{ c.notification.sender|chill_entity_render_string }} + {{ c.notification.sender|chill_entity_render_string({'at_date': c.notification.date}) }} {% else %} {{ 'notification.is_system'|trans }} @@ -53,7 +53,7 @@ {% endif %} {% for a in c.notification.addressees %} - {{ a|chill_entity_render_string }} + {{ a|chill_entity_render_string({'at_date': c.notification.date}) }} {% endfor %} {% for a in c.notification.addressesEmails %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig index 53e296d8d..e3aa98329 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig @@ -1,7 +1,7 @@ {% extends "@ChillMain/layout.html.twig" %} {% block title 'notification.show notification from %sender%'|trans( - { '%sender%': notification.sender|chill_entity_render_string } + { '%sender%': notification.sender|chill_entity_render_string({'at_date': notification.date}) } ) ~ ' ' ~ notification.title %} {% block js %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig index 2aec17cf9..0056c503d 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig @@ -31,14 +31,14 @@
    {{ 'By'|trans }} - {{ step.previous.transitionBy|chill_entity_render_box }}, + {{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}, {{ step.previous.transitionAt|format_datetime('short', 'short') }}
    {% else %}
    {{ 'workflow.Created by'|trans }}
    -
    {{ step.entityWorkflow.createdBy|chill_entity_render_box }}
    +
    {{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt}) }}
    {{ 'Le'|trans }}
    @@ -110,8 +110,8 @@ {% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}

    {{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :

    {% endif %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig index acbf97cf7..a3e2e24b9 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig @@ -42,7 +42,7 @@
    {% if step.transitionBy is not null %}
    - {{ step.transitionBy|chill_entity_render_box }} + {{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}
    {% endif %}
    @@ -76,7 +76,7 @@

    {{ 'workflow.Users allowed to apply transition'|trans }} :

      {% for u in step.destUser %} -
    • {{ u|chill_entity_render_box }}
    • +
    • {{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}
    • {% endfor %}
    {% endif %} @@ -85,7 +85,7 @@

    {{ 'workflow.Users put in Cc'|trans }} :

      {% for u in step.ccUser %} -
    • {{ u|chill_entity_render_box }}
    • +
    • {{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}
    • {% endfor %}
    {% endif %} @@ -103,7 +103,7 @@

    {{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :

      {% for u in step.destUserByAccessKey %} -
    • {{ u|chill_entity_render_box }}
    • +
    • {{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}
    • {% endfor %}
    {% endif %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/macro_breadcrumb.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/macro_breadcrumb.html.twig index 2ba893910..71d1efcf1 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/macro_breadcrumb.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/macro_breadcrumb.html.twig @@ -3,7 +3,7 @@ {% if step.previous is not null %}
  • {{ 'By'|trans ~ ' : ' }} - {{ step.previous.transitionBy|chill_entity_render_box }} + {{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}
  • {{ 'Le'|trans ~ ' : ' }} @@ -12,19 +12,19 @@
  • {{ 'workflow.For'|trans ~ ' : ' }} - {% for d in step.destUser %}{{ d|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %} + {% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %}
  • {{ 'workflow.Cc'|trans ~ ' : ' }} - {% for u in step.ccUser %}{{ u|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %} + {% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %}
  • {% else %}
  • {{ 'workflow.Created by'|trans ~ ' : ' }} - {{ step.entityWorkflow.createdBy|chill_entity_render_box }} + {{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt }) }}
  • {{ 'Le'|trans ~ ' : ' }} diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/EntityWorkflowStepNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/EntityWorkflowStepNormalizer.php index 46dbd00cc..2d924eb4d 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/EntityWorkflowStepNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/EntityWorkflowStepNormalizer.php @@ -54,7 +54,7 @@ class EntityWorkflowStepNormalizer implements NormalizerAwareInterface, Normaliz $data['transitionPreviousBy'] = $this->normalizer->normalize( $previous->getTransitionBy(), $format, - $context + [...$context, UserNormalizer::AT_DATE => $previous->getTransitionAt()] ); $data['transitionPreviousAt'] = $this->normalizer->normalize( $previous->getTransitionAt(), diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/NotificationNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/NotificationNormalizer.php index 70cd9a838..a19cd6778 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/NotificationNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/NotificationNormalizer.php @@ -47,7 +47,7 @@ class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInte 'message' => $object->getMessage(), 'relatedEntityClass' => $object->getRelatedEntityClass(), 'relatedEntityId' => $object->getRelatedEntityId(), - 'sender' => $this->normalizer->normalize($object->getSender(), $format, $context), + 'sender' => $this->normalizer->normalize($object->getSender(), $format, [...$context, UserNormalizer::AT_DATE => $object->getDate()]), 'title' => $object->getTitle(), 'entity' => null !== $entity ? $this->normalizer->normalize($entity, $format, $context) : null, ]; diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php index 9843c5dd5..e592b453e 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php @@ -19,6 +19,7 @@ use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Templating\Entity\UserRender; use libphonenumber\PhoneNumber; +use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; @@ -27,6 +28,8 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware { use NormalizerAwareTrait; + final public const AT_DATE = 'chill:user:at_date'; + final public const NULL_USER = [ 'type' => 'user', 'id' => '', @@ -38,12 +41,18 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware 'isAbsent' => false, ]; - public function __construct(private readonly UserRender $userRender) + public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) { } + /** + * @param mixed|null $format + * + * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface + */ public function normalize($object, $format = null, array $context = []) { + /** @var array{"chill:user:at_date"?: \DateTimeImmutable|\DateTime} $context */ /** @var User $object */ $userJobContext = array_merge( $context, @@ -74,18 +83,23 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)]; } + $at = $context[self::AT_DATE] ?? $this->clock->now(); + if ($at instanceof \DateTime) { + $at = \DateTimeImmutable::createFromMutable($at); + } + $data = [ 'type' => 'user', 'id' => $object->getId(), 'username' => $object->getUsername(), - 'text' => $this->userRender->renderString($object, []), + 'text' => $this->userRender->renderString($object, ['at_date' => $at]), 'text_without_absent' => $this->userRender->renderString($object, ['absence' => false]), 'label' => $object->getLabel(), 'email' => (string) $object->getEmail(), 'phonenumber' => $this->normalizer->normalize($object->getPhonenumber(), $format, $phonenumberContext), - 'user_job' => $this->normalizer->normalize($object->getUserJob(), $format, $userJobContext), + 'user_job' => $this->normalizer->normalize($object->getUserJob($at), $format, $userJobContext), 'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext), - 'main_scope' => $this->normalizer->normalize($object->getMainScope(), $format, $scopeContext), + 'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext), 'isAbsent' => $object->isAbsent(), ]; diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php index f246b0185..f8dd55bf2 100644 --- a/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php +++ b/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php @@ -12,8 +12,14 @@ declare(strict_types=1); namespace Chill\MainBundle\Templating\Entity; use Chill\MainBundle\Entity\User; -use Chill\MainBundle\Templating\TranslatableStringHelper; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; +use DateTime; +use DateTimeImmutable; +use Symfony\Component\Clock\ClockInterface; use Symfony\Contracts\Translation\TranslatorInterface; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; /** * @implements ChillEntityRenderInterface @@ -24,17 +30,32 @@ class UserRender implements ChillEntityRenderInterface 'main_scope' => true, 'user_job' => true, 'absence' => true, - 'at' => null, + 'at_date' => null, // instanceof DateTimeInterface ]; - public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) - { + public function __construct( + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly \Twig\Environment $engine, + private readonly TranslatorInterface $translator, + private readonly ClockInterface $clock, + ) { } + /** + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError + */ public function renderBox($entity, array $options): string { $opts = \array_merge(self::DEFAULT_OPTIONS, $options); + if (null === $opts['at_date']) { + $opts['at_date'] = $this->clock->now(); + } elseif ($opts['at_date'] instanceof \DateTime) { + $opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']); + } + return $this->engine->render('@ChillMain/Entity/user.html.twig', [ 'user' => $entity, 'opts' => $opts, @@ -45,16 +66,24 @@ class UserRender implements ChillEntityRenderInterface { $opts = \array_merge(self::DEFAULT_OPTIONS, $options); - $str = $entity->getLabel(); + // $immutableAtDate = $opts['at_date'] instanceOf DateTime ? DateTimeImmutable::createFromMutable($opts['at_date']) : $opts['at_date']; - if (null !== $entity->getUserJob($opts['at']) && $opts['user_job']) { - $str .= ' ('.$this->translatableStringHelper - ->localize($entity->getUserJob($opts['at'])->getLabel()).')'; + if (null === $opts['at_date']) { + $opts['at_date'] = $this->clock->now(); + } elseif ($opts['at_date'] instanceof \DateTime) { + $opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']); } - if (null !== $entity->getMainScope($opts['at']) && $opts['main_scope']) { + $str = $entity->getLabel(); + + if (null !== $entity->getUserJob($opts['at_date']) && $opts['user_job']) { $str .= ' ('.$this->translatableStringHelper - ->localize($entity->getMainScope($opts['at'])->getName()).')'; + ->localize($entity->getUserJob($opts['at_date'])->getLabel()).')'; + } + + if (null !== $entity->getMainScope($opts['at_date']) && $opts['main_scope']) { + $str .= ' ('.$this->translatableStringHelper + ->localize($entity->getMainScope($opts['at_date'])->getName()).')'; } if ($entity->isAbsent() && $opts['absence']) { diff --git a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php index 96587a24c..4a6fc032f 100644 --- a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php @@ -25,6 +25,7 @@ use libphonenumber\PhoneNumberUtil; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\Clock\MockClock; use Symfony\Component\Serializer\Exception\ExceptionInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -122,7 +123,9 @@ final class UserNormalizerTest extends TestCase $userRender = $this->prophesize(UserRender::class); $userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : ''); - $normalizer = new UserNormalizer($userRender->reveal()); + $clock = new MockClock(new \DateTimeImmutable('now')); + + $normalizer = new UserNormalizer($userRender->reveal(), $clock); $normalizer->setNormalizer(new class () implements NormalizerInterface { public function normalize($object, ?string $format = null, array $context = []) { diff --git a/src/Bundle/ChillMainBundle/Tests/Templating/Entity/UserRenderTest.php b/src/Bundle/ChillMainBundle/Tests/Templating/Entity/UserRenderTest.php new file mode 100644 index 000000000..2b0e07732 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Templating/Entity/UserRenderTest.php @@ -0,0 +1,109 @@ +setLabel(['fr' => 'assistant social']) + ->setActive(true); + $scopeA->setName(['fr' => 'service A']); + $user->setLabel('BOB ISLA'); + + $userJobB = new UserJob(); + $scopeB = new Scope(); + + $userJobB->setLabel(['fr' => 'directrice']) + ->setActive(true); + $scopeB->setName(['fr' => 'service B']); + + $userJobHistoryA = (new User\UserJobHistory()) + ->setUser($user) + ->setJob($userJobA) + ->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00')) + ->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00')); + + $userScopeHistoryA = (new User\UserScopeHistory()) + ->setUser($user) + ->setScope($scopeA) + ->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00')) + ->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00')); + + $userJobHistoryB = (new User\UserJobHistory()) + ->setUser($user) + ->setJob($userJobB) + ->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00')); + + $userScopeHistoryB = (new User\UserScopeHistory()) + ->setUser($user) + ->setScope($scopeB) + ->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00')); + + $user->getUserJobHistories()->add($userJobHistoryA); + $user->getUserScopeHistories()->add($userScopeHistoryA); + + $user->getUserJobHistories()->add($userJobHistoryB); + $user->getUserScopeHistories()->add($userScopeHistoryB); + + // Create renderer + $translatableStringHelperMock = $this->prophesize(TranslatableStringHelperInterface::class); + $translatableStringHelperMock->localize(Argument::type('array'))->will(fn ($args) => $args[0]['fr']); + + $engineMock = $this->createMock(Environment::class); + $translatorMock = $this->createMock(TranslatorInterface::class); + $clock = new MockClock(new \DateTimeImmutable('2023-12-15 12:00:00')); + + $renderer = new UserRender($translatableStringHelperMock->reveal(), $engineMock, $translatorMock, $clock); + + $optionsNoDate['at_date'] = null; + $options['at_date'] = new \DateTime('2023-11-25 12:00:00'); + $optionsTwo['at_date'] = new \DateTime('2024-01-30 12:00:00'); + + // Check that the user render for the first activity corresponds with the first user job + $expectedStringA = 'BOB ISLA (assistant social) (service A)'; + $this->assertEquals($expectedStringA, $renderer->renderString($user, $options)); + + // Check that the user render for the second activity corresponds with the second user job + $expectedStringB = 'BOB ISLA (directrice) (service B)'; + $this->assertEquals($expectedStringB, $renderer->renderString($user, $optionsTwo)); + + // Check that the user renders the job and scope that is active now, when no date is given + $expectedStringC = 'BOB ISLA (directrice) (service B)'; + $this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate)); + } +} diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index a38b7055c..03a587620 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -1507,6 +1507,16 @@ class AccompanyingPeriod implements return $this; } + public function getUserHistories(): ReadableCollection + { + return $this->userHistories; + } + + public function getCurrentUserHistory(): ?UserHistory + { + return $this->getUserHistories()->findFirst(fn (int $key, UserHistory $userHistory) => null === $userHistory->getEndDate()); + } + private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory, array $context = []): self { if (!$this->stepHistories->contains($stepHistory)) { diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php index 618918695..755f499d5 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php @@ -60,13 +60,14 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues * orphanRemoval=true * ) * - * @Serializer\Groups({"read", "docgen:read"}) + * @Serializer\Groups({"read"}) * * @ORM\OrderBy({"startDate": "DESC", "id": "DESC"}) * * @var Collection * - * @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer` + * @internal the serialization for write evaluations is handled in `accompanyingperiodworkdenormalizer` + * @internal the serialization for context docgen:read is handled in `accompanyingperiodworknormalizer` */ private Collection $accompanyingPeriodWorkEvaluations; @@ -395,21 +396,21 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues /** * @return ReadableCollection * - * @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"}) * @Serializer\Groups({"accompanying_period_work:edit"}) - * @Serializer\Groups({"accompanying_period_work:create"}) */ public function getReferrers(): ReadableCollection { $users = $this->referrersHistory ->filter(fn (AccompanyingPeriodWorkReferrerHistory $h) => null === $h->getEndDate()) ->map(fn (AccompanyingPeriodWorkReferrerHistory $h) => $h->getUser()) - ->getValues() - ; + ->getValues(); return new ArrayCollection(array_values($users)); } + /** + * @return Collection + */ public function getReferrersHistory(): Collection { return $this->referrersHistory; @@ -577,9 +578,9 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues return $this; } - public function setCreatedBy(?User $createdBy): self + public function setCreatedBy(?User $user): self { - $this->createdBy = $createdBy; + $this->createdBy = $user; return $this; } @@ -621,14 +622,14 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues public function setStartDate(\DateTimeInterface $startDate): self { - $this->startDate = $startDate; + $this->startDate = $startDate instanceof \DateTime ? \DateTimeImmutable::createFromMutable($startDate) : $startDate; return $this; } public function setUpdatedAt(\DateTimeInterface $datetime): TrackUpdateInterface { - $this->updatedAt = $datetime; + $this->updatedAt = $datetime instanceof \DateTime ? \DateTimeImmutable::createFromMutable($datetime) : $datetime; return $this; } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php index c972f453a..509dcabb0 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php @@ -167,6 +167,11 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface return $this; } + public function getCreatedBy(): ?User + { + return $this->getCreator(); + } + public function setUpdatedAt(\DateTimeInterface $updatedAt): self { $this->updatedAt = $updatedAt; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue index 05f622d00..84984fc01 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue @@ -1,6 +1,6 @@