diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 295eb3e0e..68b1bcf08 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,7 +15,9 @@ before_script: - curl -sS https://getcomposer.org/installer | php - php -d memory_limit=2G composer.phar install - php tests/app/bin/console doctrine:migrations:migrate -n - - php -d memory_limit=2G tests/app/bin/console doctrine:fixtures:load -n + - php -d memory_limit=2G tests/app/bin/console cache:clear --env=dev + - php -d memory_limit=3G tests/app/bin/console doctrine:fixtures:load -n + - php -d memory_limit=2G tests/app/bin/console cache:clear --env=test - echo "before_script finished" # Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service diff --git a/.gitlab/merge_request_templates/Default merge request.md b/.gitlab/merge_request_templates/Default merge request.md new file mode 100644 index 000000000..5d62f91eb --- /dev/null +++ b/.gitlab/merge_request_templates/Default merge request.md @@ -0,0 +1,24 @@ + +# Description of changes + + + + +# Issues related + + + +* ... +* ... + +# Tests + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index de73e3467..8e6252d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,57 @@ and this project adheres to ## Unreleased +* unnecessary whitespace removed from person banner after person-id + double parentheses removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/290) +* [person]: delete accompanying period work, including related objects (cascade) (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/36) +* [address]: Display of incomplete address adjusted. +* [household]: improve relationship graph + * add form to create/edit/delete relationship link, + * improve graph refresh mechanism + * add feature to export canvas as image (png) + +## Test releases + +### Test release 2021-11-08 + +* [person]: Display the name of a user when searching after a User (TMS) +* [person]: Add civility to the person +* [person]: Various improvements on the edit person form +* [person]: Set available_languages and available_countries as parameters for use in the edit person form +* [activity] Bugfix: documents can now be added to an activity. +* [tasks] improve tasks with filter order +* [tasks] refactor singleControllerTasks: limit the number of conditions from the context +* [validations] validation of accompanying period added: no duplicate participations or resources (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/60). +* [renderbox] If gender of person is not defined, no icon is displayed instead of neuter-icon (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/129). +* [confidential information] module added to blur confidential information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/248). +* refactor `AuthorizationHelper` and `UserACLAwareRepository` to fix constructor, and separate logic for parent role helper into `ParentRoleHelper` +* [main]: filter location and locationType in backend: exclude NULL names, only active and availableToUsers +* [activity]: perform client-side validation & show/hide fields in the "new location" modal +* [person]: normalize person with CenterResolverDispatcher and handle case where center is null or multiple in PersonRenderBox +* [docstore] voter for PersonDocument and AccompanyingCourseDocument on the 2.0 way (using VoterHelperFactory) +* [docstore] add authorization check inside controller and menu +* [activity]: fix inheritance for role `ACTIVITY FULL` and add missing acl in menu +* [person] show current address in search results +* [person] show alt names in search results +* [admin]: links to activity admin section added again. +* [household]: endDate field deleted from household edit form. +* [household]: View accompanying periods of current and old household members. +* [tasks]: different layout for task list / my tasks, and fix link to tasks in alert or in warning +* [admin]: links to activity admin section added again. +* [household]: household addresses ordered by ValidFrom date and by id to show the last created address on top. +* [socialWorkAction]: display of social issue and parent issues + banner context added. +* [DBAL dependencies] Upgrade to DBAL 3.1 +* [person]: double parentheses removed around age in banner + whitespace + +### Test release 2021-10-27 + +* [person]: delete double actions buttons on search person page +* [person]: accompanying course work: remove creation date display the list of work + handle case when end date is null +* [main]: Add new pages with a menu for managing location and location type in the admin +* [main]: Add some fixtures for location type +* [calendar]: Pass the location when transforming a calendar item (rdv) into an activity +* [calendar]: Add a user menu for "my calendar" + +### Test release 2021-10-18 * [3party]: french translation of contact and company * [3party]: show parent in list @@ -21,13 +72,14 @@ and this project adheres to * [Location]: add location system in activity and RV (calendar). User can choose in location list or create a new location. * [household]: add relationship page with dynamic data visualisation graph - ## Test releases ### Test release 2021-10-11 * Address: zoom on postal code geometry + fix origin of manually entered postal code +* in the Address vue component, order the postal code and street address by alphabetic and numeric order + * add 3 new fields to PostalCode and adapt postal code command and fixtures * [Aside activity] Fixes for aside activity @@ -87,7 +139,7 @@ and this project adheres to ## Test released -{% endblock %} + {{ encore_entry_link_tags('mod_blur') }} + {% block css %}{% endblock %} @@ -88,6 +89,7 @@ {{ encore_entry_script_tags('mod_bootstrap') }} {{ encore_entry_script_tags('mod_forkawesome') }} {{ encore_entry_script_tags('mod_ckeditor5') }} + {{ encore_entry_script_tags('mod_blur') }} {{ encore_entry_script_tags('chill') }} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js index ee4c3e11b..c372ac7a7 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js @@ -1,3 +1,5 @@ +import { fetchResults } from 'ChillMainAssets/lib/api/download.js'; + /* * Endpoint v.2 chill_api_single_accompanying_course__entity * method GET/HEAD, get AccompanyingCourse Instance @@ -84,7 +86,8 @@ const postParticipation = (id, payload, method) => { }) .then(response => { if (response.ok) { return response.json(); } - throw { msg: 'Error while sending AccompanyingPeriod Course participation.', sta: response.status, txt: response.statusText, err: new Error(), body: response.body }; + // TODO: adjust message according to status code? Or how to access the message from the violation array? + throw { msg: 'Error while sending AccompanyingPeriod Course participation', sta: response.status, txt: response.statusText, err: new Error(), body: response.body }; }); }; @@ -168,11 +171,8 @@ const postSocialIssue = (id, body, method) => { const getUsers = () => { const url = `/api/1.0/main/user.json`; - return fetch(url) - .then(response => { - if (response.ok) { return response.json(); } - throw { msg: 'Error while retriving users.', sta: response.status, txt: response.statusText, err: new Error(), body: response.body }; - }); + + return fetchResults(url); }; const whoami = () => { @@ -216,8 +216,6 @@ const addScope = (id, scope) => { const removeScope = (id, scope) => { const url = `/api/1.0/person/accompanying-course/${id}/scope.json`; - console.log(url); - console.log(scope); return fetch(url, { method: 'DELETE', @@ -235,6 +233,12 @@ const removeScope = (id, scope) => { }); }; +const getReferrersSuggested = (course) => { + const url = `/api/1.0/person/accompanying-course/${course.id}/referrers-suggested.json`; + + return fetchResults(url); +} + export { getAccompanyingCourse, patchAccompanyingCourse, @@ -249,4 +253,5 @@ export { postSocialIssue, addScope, removeScope, + getReferrersSuggested, }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue index 30001028c..30ad6afe0 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue @@ -10,13 +10,13 @@ @@ -47,18 +47,18 @@ export default { }, methods: { getOptions() { - //console.log('loading origins list'); getListOrigins().then(response => new Promise((resolve, reject) => { this.options = response.results; resolve(); })); }, updateOrigin(value) { - //console.log('value', value); + console.log('value', value); this.$store.dispatch('updateOrigin', value); }, transText ({ text }) { - return text.fr //TODO multilang + const parsedText = JSON.parse(text); + return parsedText.fr; }, } } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated/ParticipationItem.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated/ParticipationItem.vue index e19dfa05e..fd3f59cef 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated/ParticipationItem.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated/ParticipationItem.vue @@ -9,6 +9,7 @@ addAltNames: true, addAge : true, hLevel : 3, + isConfidential : false, }" :person="participation.person" :returnPath="getAccompanyingCourseReturnPath"> diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue index d1ecfbed1..c39e043c6 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue @@ -15,9 +15,20 @@ v-bind:searchable="true" v-bind:placeholder="$t('referrer.placeholder')" v-model="value" - v-bind:options="options" + v-bind:options="users" @select="updateReferrer"> + + + + +
@@ -41,30 +52,29 @@ import VueMultiselect from 'vue-multiselect'; import { getUsers, whoami } from '../api'; import { mapState } from 'vuex'; +import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge"; export default { name: "Referrer", - components: { VueMultiselect }, - data() { - return { - options: [] - } + components: { + UserRenderBoxBadge, + VueMultiselect, }, computed: { ...mapState({ value: state => state.accompanyingCourse.user, + users: state => state.users, + referrersSuggested: state => { + return state.referrersSuggested.filter(u => { + if (null === state.accompanyingCourse.user) { + return true; + } + return state.accompanyingCourse.user.id !== u.id; + }) + }, }), }, - mounted() { - this.getOptions(); - }, methods: { - getOptions() { - getUsers().then(response => new Promise((resolve, reject) => { - this.options = response.results; - resolve(); - })); - }, updateReferrer(value) { //console.log('value', value); this.$store.dispatch('updateReferrer', value); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Requestor.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Requestor.vue index cb4b243bb..6aedd373f 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Requestor.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Requestor.vue @@ -3,51 +3,60 @@

{{ $t('requestor.title') }}

-
+
- - - - - - - - + + + + + + +
  • @@ -59,6 +68,57 @@
+ +
+ + + + + + + + + +
+
@@ -82,6 +142,7 @@ import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'; import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue'; import PersonRenderBox from '../../_components/Entity/PersonRenderBox.vue'; import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue'; +import Confidential from 'ChillMainAssets/vuejs/_components/Confidential.vue'; export default { name: 'Requestor', @@ -90,7 +151,9 @@ export default { OnTheFly, PersonRenderBox, ThirdPartyRenderBox, + Confidential }, + props: ['isAnonymous'], data() { return { addPersons: { @@ -149,4 +212,8 @@ div.flex-table { margin-top: 1em; } } + +.confidential { + display: block; +} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/ResourceItem.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/ResourceItem.vue index bd70fa090..7f209f0d4 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/ResourceItem.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/ResourceItem.vue @@ -2,7 +2,7 @@ + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue index 530a14e95..eb7793839 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue @@ -35,7 +35,7 @@
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list.html.twig index a53a7c0d0..c5797a4b2 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list.html.twig @@ -77,7 +77,7 @@

{{ 'Participants'|trans }}

{%- if options['addInfo'] -%} {% set gender = (person.gender == 'woman') ? 'fa-venus' : - (person.gender == 'man') ? 'fa-mars' : 'fa-neuter' %} + (person.gender == 'man') ? 'fa-mars' : (person.gender == 'neuter') ? 'fa-neuter' : 'fa-genderless' %} {% set genderTitle = (person.gender == 'woman') ? 'woman' : - (person.gender == 'man') ? 'man' : 'neuter' %} + (person.gender == 'man') ? 'man' : (person.gender == 'neuter') ? 'neuter' : 'Not given'|trans %}

@@ -95,7 +95,7 @@ {%- if options['addAge'] -%} - ({{ 'years_old'|trans({ 'age': person.age }) }}) + {{- 'years_old'|trans({ 'age': person.age }) -}} {%- endif -%} {%- endif -%} @@ -123,13 +123,15 @@

{% endfor %}
- - - {% else %} - - {% endif %} {% if preview == false %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/view.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/view.html.twig index a67723f27..0351d0ba1 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/view.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/view.html.twig @@ -51,6 +51,15 @@ This view should receive those arguments:

{{ 'General information'|trans }}

+ {% if person.civility is not null %} +
{{ 'Civility'|trans }} :
+
+ {% if person.civility.name|length > 0 %} + {{ person.civility.name|first }} + {% endif %} +
+ {% endif %} +
{{ 'First name'|trans }} :
{{ person.firstName }}
@@ -93,14 +102,14 @@ This view should receive those arguments: {%- endif -%} {%- if chill_person.fields.country_of_birth == 'visible' -%} -
{{ 'Country of birth'|trans }} :
-
{% apply spaceless %} - {% if person.countryOfBirth is not null %} - {{ person.countryOfBirth.name|localize_translatable_string }} - {% else %} - {{ 'Unknown country of birth'|trans }} - {% endif %} - {% endapply %}
+
{{ 'Country of birth'|trans }} :
+
{% apply spaceless %} + {% if person.countryOfBirth is not null %} + {{ person.countryOfBirth.name|localize_translatable_string }} + {% else %} + {{ 'Unknown country of birth'|trans }} + {% endif %} + {% endapply %}
{%- endif -%}
@@ -198,10 +207,10 @@ This view should receive those arguments: {%- endif -%} {%- if chill_person.fields.phonenumber == 'visible' -%} -
-
{{ 'Phonenumber'|trans }} :
-
{% if person.phonenumber is not empty %}
{{ person.phonenumber|chill_format_phonenumber }}
{% else %}{{ 'No data given'|trans }}{% endif %}
-
+
+
{{ 'Phonenumber'|trans }} :
+
{% if person.phonenumber is not empty %}
{{ person.phonenumber|chill_format_phonenumber }}
{% else %}{{ 'No data given'|trans }}{% endif %}
+
{% endif %} {%- if chill_person.fields.mobilenumber == 'visible' -%} @@ -261,4 +270,4 @@ This view should receive those arguments: {% endif %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/Search/PersonSearch.php b/src/Bundle/ChillPersonBundle/Search/PersonSearch.php index f62b90006..7fff34d7d 100644 --- a/src/Bundle/ChillPersonBundle/Search/PersonSearch.php +++ b/src/Bundle/ChillPersonBundle/Search/PersonSearch.php @@ -263,8 +263,9 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf public function convertTermsToFormData(array $terms) { - foreach(['firstname', 'lastname', 'gender', '_default'] - as $key) { + $data = []; + + foreach(['firstname', 'lastname', 'gender', '_default'] as $key) { $data[$key] = $terms[$key] ?? null; } diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php index 380112a12..019f3989e 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php @@ -19,6 +19,7 @@ namespace Chill\PersonBundle\Serializer\Normalizer; use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher; use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Person; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; @@ -48,16 +49,22 @@ class PersonNormalizer implements private PersonRepository $repository; + private CenterResolverDispatcher $centerResolverDispatcher; + use NormalizerAwareTrait; use ObjectToPopulateTrait; use DenormalizerAwareTrait; - public function __construct(ChillEntityRenderExtension $render, PersonRepository $repository) - { + public function __construct( + ChillEntityRenderExtension $render, + PersonRepository $repository, + CenterResolverDispatcher $centerResolverDispatcher + ) { $this->render = $render; $this->repository = $repository; + $this->centerResolverDispatcher = $centerResolverDispatcher; } public function normalize($person, string $format = null, array $context = array()) @@ -74,7 +81,7 @@ class PersonNormalizer implements 'lastName' => $person->getLastName(), 'birthdate' => $this->normalizer->normalize($person->getBirthdate()), 'deathdate' => $this->normalizer->normalize($person->getDeathdate()), - 'center' => $this->normalizer->normalize($person->getCenter()), + 'center' => $this->normalizer->normalize($this->centerResolverDispatcher->resolveCenter($person)), 'phonenumber' => $person->getPhonenumber(), 'mobilenumber' => $person->getMobilenumber(), 'altNames' => $this->normalizeAltNames($person->getAltNames()), diff --git a/src/Bundle/ChillPersonBundle/Templating/Entity/SocialActionRender.php b/src/Bundle/ChillPersonBundle/Templating/Entity/SocialActionRender.php index 7da1a8cad..c68e300cf 100644 --- a/src/Bundle/ChillPersonBundle/Templating/Entity/SocialActionRender.php +++ b/src/Bundle/ChillPersonBundle/Templating/Entity/SocialActionRender.php @@ -38,7 +38,7 @@ class SocialActionRender implements ChillEntityRenderInterface { /** @var $socialAction SocialAction */ $options = \array_merge(self::DEFAULT_ARGS, $options); - $titles[] = $this->translatableStringHelper->localize($socialAction->getTitle()); + $titles = [$this->translatableStringHelper->localize($socialAction->getTitle())]; while ($socialAction->hasParent()) { $socialAction = $socialAction->getParent(); diff --git a/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php b/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php index 5ed80182c..80254d989 100644 --- a/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php +++ b/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php @@ -38,8 +38,7 @@ final class SocialIssueRender implements ChillEntityRenderInterface /** @var $socialIssue SocialIssue */ $options = array_merge(self::DEFAULT_ARGS, $options); - $titles[] = $this->translatableStringHelper - ->localize($socialIssue->getTitle()); + $titles = [$this->translatableStringHelper->localize($socialIssue->getTitle())]; // loop to parent, until root while ($socialIssue->hasParent()) { diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php index a24e94bfa..78376b8bd 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php @@ -325,6 +325,19 @@ class AccompanyingCourseApiControllerTest extends WebTestCase $this->period = $period; } + /** + * @dataProvider dataGenerateRandomAccompanyingCourse + */ + public function testReferralAvailable(int $personId, int $periodId) + { + $this->client->request( + Request::METHOD_POST, + sprintf('/api/1.0/person/accompanying-course/%d/referrers-suggested.json', $periodId) + ); + + $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); + } + /** * * @dataProvider dataGenerateRandomAccompanyingCourse diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php index 3da325a9e..9b7b97a4f 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php @@ -193,8 +193,6 @@ class HouseholdMemberControllerTest extends WebTestCase $form = $crawler->selectButton('Enregistrer') ->form(); - $form['household_member[endDate]'] = (new \DateTime('tomorrow')) - ->format('Y-m-d'); $crawler = $client->submit($form); diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/PersonTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/PersonTest.php index dbef739d1..5b8dce6b2 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Entity/PersonTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/PersonTest.php @@ -2,8 +2,8 @@ /* * Chill is a software for social workers - * - * Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS, + * + * Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS, * , * * This program is free software: you can redistribute it and/or modify @@ -38,57 +38,57 @@ use Generator; class PersonTest extends \PHPUnit\Framework\TestCase { /** - * Test the creation of an accompanying, its closure and the access to + * Test the creation of an accompanying, its closure and the access to * the current accompaniying period via the getCurrentAccompanyingPeriod * function. */ public function testGetCurrentAccompanyingPeriod() { - $d = new \DateTime('yesterday'); + $d = new \DateTime('yesterday'); $p = new Person(); $p->addAccompanyingPeriod(new AccompanyingPeriod($d)); - + $period = $p->getCurrentAccompanyingPeriod(); - + $this->assertInstanceOf(AccompanyingPeriod::class, $period); $this->assertTrue($period->isOpen()); $this->assertEquals($d, $period->getOpeningDate()); - + //close and test $period->setClosingDate(new \DateTime('tomorrow')); - + $shouldBeNull = $p->getCurrentAccompanyingPeriod(); $this->assertNull($shouldBeNull); } - + /** * Test if the getAccompanyingPeriodsOrdered function return a list of * periods ordered ascendency. */ public function testAccompanyingPeriodOrderWithUnorderedAccompanyingPeriod() - { + { $d = new \DateTime("2013/2/1"); $p = new Person(); $p->addAccompanyingPeriod(new AccompanyingPeriod($d)); - + $e = new \DateTime("2013/3/1"); $period = $p->getCurrentAccompanyingPeriod()->setClosingDate($e); $p->close($period); - + $f = new \DateTime("2013/1/1"); $p->open(new AccompanyingPeriod($f)); - - $g = new \DateTime("2013/4/1"); + + $g = new \DateTime("2013/4/1"); $period = $p->getCurrentAccompanyingPeriod()->setClosingDate($g); $p->close($period); - + $r = $p->getAccompanyingPeriodsOrdered(); - + $date = $r[0]->getOpeningDate()->format('Y-m-d'); - + $this->assertEquals($date, '2013-01-01'); } - + /** * Test if the getAccompanyingPeriodsOrdered function, for periods * starting at the same time order regarding to the closing date. @@ -97,25 +97,25 @@ class PersonTest extends \PHPUnit\Framework\TestCase $d = new \DateTime("2013/2/1"); $p = new Person(); $p->addAccompanyingPeriod(new AccompanyingPeriod($d)); - - $g = new \DateTime("2013/4/1"); + + $g = new \DateTime("2013/4/1"); $period = $p->getCurrentAccompanyingPeriod()->setClosingDate($g); $p->close($period); - + $f = new \DateTime("2013/2/1"); $p->open(new AccompanyingPeriod($f)); - + $e = new \DateTime("2013/3/1"); $period = $p->getCurrentAccompanyingPeriod()->setClosingDate($e); $p->close($period); $r = $p->getAccompanyingPeriodsOrdered(); - + $date = $r[0]->getClosingDate()->format('Y-m-d'); - + $this->assertEquals($date, '2013-03-01'); } - + /** * Test if the function checkAccompanyingPeriodIsNotCovering returns * the good constant when two periods are collapsing : a period @@ -125,23 +125,23 @@ class PersonTest extends \PHPUnit\Framework\TestCase $d = new \DateTime("2013/2/1"); $p = new Person(); $p->addAccompanyingPeriod(new AccompanyingPeriod($d)); - + $e = new \DateTime("2013/3/1"); $period = $p->getCurrentAccompanyingPeriod()->setClosingDate($e); $p->close($period); - + $f = new \DateTime("2013/1/1"); $p->open(new AccompanyingPeriod($f)); - - $g = new \DateTime("2013/4/1"); + + $g = new \DateTime("2013/4/1"); $period = $p->getCurrentAccompanyingPeriod()->setClosingDate($g); $p->close($period); - + $r = $p->checkAccompanyingPeriodsAreNotCollapsing(); - + $this->assertEquals($r['result'], Person::ERROR_PERIODS_ARE_COLLAPSING); } - + /** * Test if the function checkAccompanyingPeriodIsNotCovering returns * the good constant when two periods are collapsing : a period is open @@ -151,68 +151,19 @@ class PersonTest extends \PHPUnit\Framework\TestCase $d = new \DateTime("2013/2/1"); $p = new Person(); $p->addAccompanyingPeriod(new AccompanyingPeriod($d)); - + $e = new \DateTime("2013/3/1"); $period = $p->getCurrentAccompanyingPeriod()->setClosingDate($e); $p->close($period); - + $f = new \DateTime("2013/1/1"); $p->open(new AccompanyingPeriod($f)); - + $r = $p->checkAccompanyingPeriodsAreNotCollapsing(); - + $this->assertEquals($r['result'], Person::ERROR_ADDIND_PERIOD_AFTER_AN_OPEN_PERIOD); } - public function dateProvider(): Generator - { - yield [(DateTime::createFromFormat('Y-m-d', '2021-01-05'))->settime(0, 0)]; - yield [(DateTime::createFromFormat('Y-m-d', '2021-02-05'))->settime(0, 0)]; - yield [(DateTime::createFromFormat('Y-m-d', '2021-03-05'))->settime(0, 0)]; - } - - /** - * @dataProvider dateProvider - */ - public function testGetLastAddress(DateTime $date) - { - $p = new Person($date); - - // Make sure that there is no last address. - $this::assertNull($p->getLastAddress()); - - // Take an arbitrary date before the $date in parameter. - $addressDate = clone $date; - - // 1. Smoke test: Test that the first address added is the last one. - $address1 = (new Address())->setValidFrom($addressDate->sub(new DateInterval('PT180M'))); - $p->addAddress($address1); - - $this::assertCount(1, $p->getAddresses()); - $this::assertSame($address1, $p->getLastAddress()); - - // 2. Add an older address, which should not be the last address. - $addressDate2 = clone $addressDate; - $address2 = (new Address())->setValidFrom($addressDate2->sub(new DateInterval('PT30M'))); - $p->addAddress($address2); - - $this::assertCount(2, $p->getAddresses()); - $this::assertSame($address1, $p->getLastAddress()); - - // 3. Add a newer address, which should be the last address. - $addressDate3 = clone $addressDate; - $address3 = (new Address())->setValidFrom($addressDate3->add(new DateInterval('PT30M'))); - $p->addAddress($address3); - - $this::assertCount(3, $p->getAddresses()); - $this::assertSame($address3, $p->getLastAddress()); - - // 4. Get the last address from a specific date. - $this::assertEquals($address1, $p->getLastAddress($addressDate)); - $this::assertEquals($address2, $p->getLastAddress($addressDate2)); - $this::assertEquals($address3, $p->getLastAddress($addressDate3)); - } - public function testIsSharingHousehold() { $person = new Person(); diff --git a/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/LocationValidity.php b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/LocationValidity.php index 75c10ca75..b1b660596 100644 --- a/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/LocationValidity.php +++ b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/LocationValidity.php @@ -11,7 +11,7 @@ class LocationValidity extends Constraint { public $messagePersonLocatedMustBeAssociated = "The person where the course is located must be associated to the course. Change course's location before removing the person."; - public $messagePeriodMustRemainsLocated = "The period must remains located"; + public $messagePeriodMustRemainsLocated = "The period must remain located"; public function getTargets() { diff --git a/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ParticipationOverlap.php b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ParticipationOverlap.php new file mode 100644 index 000000000..38d5516b5 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ParticipationOverlap.php @@ -0,0 +1,15 @@ +getStartDate()->getTimezone()); + + foreach ($participations as $participation) { + + if (!$participation instanceof AccompanyingPeriodParticipation) { + throw new UnexpectedTypeException($participation, AccompanyingPeriodParticipation::class); + } + + $personId = $participation->getPerson()->getId(); + + $particpationList[$personId][] = $participation; + + } + + foreach ($particpationList as $group) { + if (count($group) > 1) { + foreach ($group as $p) { + $overlaps->add($p->getStartDate(), $p->getEndDate(), $p->getId()); + } + } + } + + $overlaps->compute(); + + if ($overlaps->hasIntersections()) { + foreach ($overlaps->getIntersections() as list($start, $end, $ids)) { + $msg = $end === null ? $constraint->message : + $constraint->message; + + $this->context->buildViolation($msg) + ->setParameters([ + '{{ start }}' => $start->format('d-m-Y'), + '{{ end }}' => $end === null ? null : $end->format('d-m-Y'), + '{{ ids }}' => $ids, + ]) + ->addViolation(); + } + } + + } +} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ResourceDuplicateCheck.php b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ResourceDuplicateCheck.php new file mode 100644 index 000000000..bfc9d62af --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ResourceDuplicateCheck.php @@ -0,0 +1,16 @@ +personRender = $personRender; + $this->thirdpartyRender = $thirdPartyRender; + } + + public function validate($resources, Constraint $constraint) + { + if (!$constraint instanceof ResourceDuplicateCheck) { + throw new UnexpectedTypeException($constraint, ParticipationOverlap::class); + } + + if (!$resources instanceof Collection) { + throw new UnexpectedTypeException($resources, Collection::class); + } + + $resourceList = []; + + foreach ($resources as $resource) { + $id = ($resource->getResource() instanceof Person ? 'p' : + 't').$resource->getResource()->getId(); + + if (\in_array($id, $resourceList, true)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ name }}', $resource->getResource() instanceof Person ? $this->personRender->renderString($resource->getResource(), []) : + $this->thirdpartyRender->renderString($resource->getResource(), [])) + ->addViolation(); + } + + $resourceList[] = $id; + + } + + } + +} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/Widget/PersonListWidget.php b/src/Bundle/ChillPersonBundle/Widget/PersonListWidget.php index 2dcbb8230..d8e61f499 100644 --- a/src/Bundle/ChillPersonBundle/Widget/PersonListWidget.php +++ b/src/Bundle/ChillPersonBundle/Widget/PersonListWidget.php @@ -22,7 +22,7 @@ namespace Chill\PersonBundle\Widget; use Chill\MainBundle\Templating\Widget\WidgetInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query\Expr; -use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Types\Types; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; @@ -33,11 +33,11 @@ use Chill\CustomFieldsBundle\Entity\CustomField; use Twig\Environment; /** - * add a widget with person list. - * + * add a widget with person list. + * * The configuration is defined by `PersonListWidgetFactory` - * - * If the options 'custom_fields' is used, the custom fields entity will be + * + * If the options 'custom_fields' is used, the custom fields entity will be * queried from the db and transmitted to the view under the `customFields` variable. */ class PersonListWidget implements WidgetInterface @@ -48,33 +48,33 @@ class PersonListWidget implements WidgetInterface * @var PersonRepository */ protected $personRepository; - + /** * The entity manager * * @var EntityManager */ protected $entityManager; - + /** * the authorization helper - * + * * @var AuthorizationHelper; */ protected $authorizationHelper; - + /** * * @var TokenStorage */ protected $tokenStorage; - + /** * * @var UserInterface */ protected $user; - + public function __construct( PersonRepository $personRepostory, EntityManager $em, @@ -88,26 +88,26 @@ class PersonListWidget implements WidgetInterface } /** - * + * * @param type $place * @param array $context * @param array $config * @return string */ public function render(Environment $env, $place, array $context, array $config) - { + { $numberOfItems = $config['number_of_items'] ?? 20; - + $qb = $this->personRepository ->createQueryBuilder('person'); - + // show only the person from the authorized centers $and = $qb->expr()->andX(); $centers = $this->authorizationHelper ->getReachableCenters($this->getUser(), new Role(PersonVoter::SEE)); $and->add($qb->expr()->in('person.center', ':centers')); $qb->setParameter('centers', $centers); - + // add the "only active" query if (\array_key_exists('only_active', $config) && $config['only_active'] === true) { @@ -123,37 +123,37 @@ class PersonListWidget implements WidgetInterface (new Expr())->between(':now', 'ap.openingDate', 'ap.closingDate') ); $and->add($or); - $qb->setParameter('now', new \DateTime(), Type::DATE); + $qb->setParameter('now', new \DateTime(), Types::DATE_MUTABLE); } - + if (\array_key_exists('filtering_class', $config) && $config['filtering_class'] !== NULL) { $filteringClass = new $config['filtering_class']; if ( ! $filteringClass instanceof PersonListWidget\PersonFilteringInterface) { throw new \UnexpectedValueException(sprintf("the class %s does not " - . "implements %s", $config['filtering_class'], + . "implements %s", $config['filtering_class'], PersonListWidget\PersonFilteringInterface::class)); } - $ids = $filteringClass->getPersonIds($this->entityManager, + $ids = $filteringClass->getPersonIds($this->entityManager, $this->getUser()); $in = (new Expr())->in('person.id', ':ids'); $and->add($in); $qb->setParameter('ids', $ids); } - + // adding the where clause to the query $qb->where($and); - + // ordering the query by lastname, firstname $qb->addOrderBy('person.lastName', 'ASC') ->addOrderBy('person.firstName', 'ASC'); - - + + $qb->setFirstResult(0)->setMaxResults($numberOfItems); - + $persons = $qb->getQuery()->getResult(); - - // get some custom field when the view is overriden and we want to + + // get some custom field when the view is overriden and we want to // show some custom field in the overriden view. $cfields = array(); if (isset($config['custom_fields'])) { @@ -166,39 +166,39 @@ class PersonListWidget implements WidgetInterface $cfields[$cf->getSlug()] = $cf; } } - } + } return $env->render( 'ChillPersonBundle:Widget:homepage_person_list.html.twig', array( 'persons' => $persons, 'customFields' => $cfields - + ) ); } - + /** - * + * * @return UserInterface * @throws \RuntimeException */ private function getUser() { $token = $this->tokenStorage->getToken(); - + if ($token === null) { throw new \RuntimeException("the token should not be null"); } - + $user = $token->getUser(); - + if (!$user instanceof UserInterface || $user == null) { throw new \RuntimeException("the user should implement UserInterface. " . "Are you logged in ?"); } - + return $user; } -} \ No newline at end of file +} diff --git a/src/Bundle/ChillPersonBundle/Widget/PersonListWidget/PersonFilteringInterface.php b/src/Bundle/ChillPersonBundle/Widget/PersonListWidget/PersonFilteringInterface.php index 555884cc1..d1de741ab 100644 --- a/src/Bundle/ChillPersonBundle/Widget/PersonListWidget/PersonFilteringInterface.php +++ b/src/Bundle/ChillPersonBundle/Widget/PersonListWidget/PersonFilteringInterface.php @@ -27,7 +27,7 @@ use Chill\MainBundle\Entity\User; /** * Interface to implement on classes called in configuration for * PersonListWidget (`person_list`), under the key `filtering_class` : - * + * * ``` * widgets: * homepage: @@ -35,41 +35,41 @@ use Chill\MainBundle\Entity\User; * # where \FQDN\To\Class implements PersonFiltering * class_filtering: \FQDN\To\Class * ``` - * + * */ -interface PersonFilteringInterface +interface PersonFilteringInterface { /** * Return an array of persons id to show. - * - * Those ids are inserted into the query like this (where ids is the array + * + * Those ids are inserted into the query like this (where ids is the array * returned by this class) : - * + * * ``` - * SELECT p FROM ChillPersonBundle:Persons p + * SELECT p FROM ChillPersonBundle:Persons p * WHERE p.id IN (:ids) - * AND + * AND * -- security/authorization statement: restraint person to authorized centers * p.center in :authorized_centers * ``` - * + * * Example of use : filtering based on custom field data : * ``` - + class HomepagePersonFiltering implements PersonFilteringInterface { public function getPersonIds(EntityManager $em, User $user) { $rsmBuilder = new ResultSetMappingBuilder($em); - $rsmBuilder->addScalarResult('id', 'id', Type::BIGINT); - + $rsmBuilder->addScalarResult('id', 'id', Types::BIGINT); + $personTable = $em->getClassMetadata('ChillPersonBundle:Person') ->getTableName(); $personIdColumn = $em->getClassMetadata('ChillPersonBundle:Person') ->getColumnName('id'); $personCfDataColumn = $em->getClassMetadata('ChillPersonBundle:Person') ->getColumnName('cfData'); - + return $em->createNativeQuery(sprintf("SELECT %s FROM %s WHERE " . "jsonb_exists(%s, 'school-2fb5440e-192c-11e6-b2fd-74d02b0c9b55')", $personIdColumn, $personTable, $personCfDataColumn), $rsmBuilder) @@ -77,7 +77,7 @@ interface PersonFilteringInterface } } * ``` - * + * * @param EntityManager $em * @return int[] an array of persons id to show */ diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index e4a44fdc0..5e1f0d937 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -480,7 +480,7 @@ paths: /1.0/person/accompanying-course/{id}.json: get: tags: - - person + - accompanying-course summary: "Return the description for an accompanying course (accompanying period)" parameters: - name: id @@ -567,7 +567,7 @@ paths: /1.0/person/accompanying-course/{id}/requestor.json: post: tags: - - person + - accompanying-course summary: "Add a requestor to the accompanying course" parameters: - name: id @@ -609,7 +609,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the requestor for the accompanying course" parameters: - name: id @@ -633,7 +633,7 @@ paths: /1.0/person/accompanying-course/{id}/participation.json: post: tags: - - person + - accompanying-course summary: "Add a participant to the accompanying course" parameters: - name: id @@ -662,7 +662,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the participant for the accompanying course" parameters: - name: id @@ -693,7 +693,7 @@ paths: /1.0/person/accompanying-course/{id}/resource.json: post: tags: - - person + - accompanying-course summary: "Add a resource to the accompanying course" parameters: - name: id @@ -738,7 +738,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the resource" parameters: - name: id @@ -769,7 +769,7 @@ paths: /1.0/person/accompanying-course/{id}/comment.json: post: tags: - - person + - accompanying-course summary: "Add a comment to the accompanying course" parameters: - name: id @@ -807,7 +807,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the comment" parameters: - name: id @@ -838,7 +838,7 @@ paths: /1.0/person/accompanying-course/{id}/scope.json: post: tags: - - person + - accompanying-course summary: "Add a scope to the accompanying course" parameters: - name: id @@ -872,7 +872,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the scope" parameters: - name: id @@ -903,7 +903,7 @@ paths: /1.0/person/accompanying-course/{id}/socialissue.json: post: tags: - - person + - accompanying-course summary: "Add a social issue to the accompanying course" parameters: - name: id @@ -937,7 +937,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the social issue" parameters: - name: id @@ -964,11 +964,31 @@ paths: description: "OK" 422: description: "object with validation errors" + /1.0/person/accompanying-course/{id}/referrers-suggested.json: + get: + tags: + - accompanying-course + summary: "get a list of available referral for a given accompanying cours" + parameters: + - name: id + in: path + required: true + description: The accompanying period's id + schema: + type: integer + format: integer + minimum: 1 + responses: + 401: + description: "Unauthorized" + 404: + description: "Not found" + 200: + description: "OK" /1.0/person/accompanying-course/{id}/work.json: post: tags: - - person - accompanying-course-work summary: "Add a work (AccompanyingPeriodwork) to the accompanying course" parameters: diff --git a/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml b/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml index 88e70c9a8..d9e784239 100644 --- a/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml @@ -13,3 +13,10 @@ services: entity: 'Chill\PersonBundle\Entity\AccompanyingPeriod' lazy: true method: preUpdateAccompanyingPeriod + + Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestion: + autowire: true + autoconfigure: true + + Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface: '@Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestion' + diff --git a/src/Bundle/ChillPersonBundle/config/services/controller.yaml b/src/Bundle/ChillPersonBundle/config/services/controller.yaml index 45a656e89..b03ccf966 100644 --- a/src/Bundle/ChillPersonBundle/config/services/controller.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/controller.yaml @@ -41,6 +41,7 @@ services: tags: ['controller.service_arguments'] Chill\PersonBundle\Controller\AccompanyingCourseApiController: + autoconfigure: true autowire: true tags: ['controller.service_arguments'] diff --git a/src/Bundle/ChillPersonBundle/config/services/form.yaml b/src/Bundle/ChillPersonBundle/config/services/form.yaml index dacfa41de..2390005cf 100644 --- a/src/Bundle/ChillPersonBundle/config/services/form.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/form.yaml @@ -7,8 +7,9 @@ services: Chill\PersonBundle\Form\PersonType: arguments: - - '%chill_person.person_fields%' - - '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper' + $personFieldsConfiguration: '%chill_person.person_fields%' + $configAltNamesHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper' + $translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper' tags: - { name: form.type, alias: '@chill.person.form.person_creation' } diff --git a/src/Bundle/ChillPersonBundle/config/services/validator.yaml b/src/Bundle/ChillPersonBundle/config/services/validator.yaml new file mode 100644 index 000000000..435f2f5b5 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/config/services/validator.yaml @@ -0,0 +1,6 @@ +services: + Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod: + autowire: true + autoconfigure: true + tags: ['validator.service_arguments'] + \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20150607231010.php b/src/Bundle/ChillPersonBundle/migrations/Version20150607231010.php index 71a603a2f..a7384e2f4 100644 --- a/src/Bundle/ChillPersonBundle/migrations/Version20150607231010.php +++ b/src/Bundle/ChillPersonBundle/migrations/Version20150607231010.php @@ -17,7 +17,6 @@ class Version20150607231010 extends AbstractMigration . 'recorded.'; } - /** * @param Schema $schema */ @@ -28,36 +27,26 @@ class Version20150607231010 extends AbstractMigration 'Migration can only be executed safely on \'postgresql\'.' ); - // retrieve center for setting a default center - $centers = $this->connection->fetchAll('SELECT id FROM centers'); - - if (count($centers) > 0) { - $defaultCenterId = $centers[0]['id']; - } else { // if no center, performs other checks - //check if there are data in person table - $nbPeople = $this->connection->fetchColumn('SELECT count(*) FROM person'); - - if ($nbPeople > 0) { - // we have data ! We have to create a center ! - $newCenterId = $this->connection->fetchColumn('SELECT nextval(\'centers_id_seq\');'); - $this->addSql( - 'INSERT INTO centers (id, name) VALUES (:id, :name)', - ['id' => $newCenterId, 'name' => 'Auto-created center'] - ); - $defaultCenterId = $newCenterId; - } - } - $this->addSql('ALTER TABLE person ADD center_id INT'); + // retrieve center for setting a default center + $stmt = $this->connection->executeQuery("SELECT id FROM centers ORDER BY id ASC LIMIT 1"); + $center = $stmt->fetchOne(); + + if ($center !== false) { + $defaultCenterId = $center['id']; + } + if (isset($defaultCenterId)) { $this->addSql('UPDATE person SET center_id = :id', array('id' => $defaultCenterId)); } + // this will fail if a person is defined, which should not happen any more at this + // time of writing (2021-11-09) + $this->addSql('ALTER TABLE person ALTER center_id SET NOT NULL'); $this->addSql('ALTER TABLE person ' . 'ADD CONSTRAINT FK_person_center FOREIGN KEY (center_id) ' . 'REFERENCES centers (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE person ALTER center_id SET NOT NULL'); $this->addSql('CREATE INDEX IDX_person_center ON person (center_id)'); } diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20200310090632.php b/src/Bundle/ChillPersonBundle/migrations/Version20200310090632.php index 3a32f4bfc..52d4b0e88 100644 --- a/src/Bundle/ChillPersonBundle/migrations/Version20200310090632.php +++ b/src/Bundle/ChillPersonBundle/migrations/Version20200310090632.php @@ -15,7 +15,6 @@ final class Version20200310090632 extends AbstractMigration $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); $this->addSql('ALTER TABLE chill_person_closingmotive ADD parent_id INT DEFAULT NULL'); - $this->addSql('COMMENT ON COLUMN chill_person_closingmotive.name IS \'(DC2Type:json_array)\''); $this->addSql('ALTER TABLE chill_person_closingmotive ADD CONSTRAINT FK_92351ECE727ACA70 FOREIGN KEY (parent_id) REFERENCES chill_person_closingmotive (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); $this->addSql('CREATE INDEX IDX_92351ECE727ACA70 ON chill_person_closingmotive (parent_id)'); $this->addsql("ALTER TABLE chill_person_closingmotive ADD ordering DOUBLE PRECISION DEFAULT '0' NOT NULL;"); diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20210419112619.php b/src/Bundle/ChillPersonBundle/migrations/Version20210419112619.php index 919fe4cff..89828f862 100644 --- a/src/Bundle/ChillPersonBundle/migrations/Version20210419112619.php +++ b/src/Bundle/ChillPersonBundle/migrations/Version20210419112619.php @@ -24,6 +24,5 @@ final class Version20210419112619 extends AbstractMigration public function down(Schema $schema) : void { - $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_closingmotive.name IS \'(DC2Type:json_array)\''); } } diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20211020131133.php b/src/Bundle/ChillPersonBundle/migrations/Version20211020131133.php new file mode 100644 index 000000000..031356cd3 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20211020131133.php @@ -0,0 +1,31 @@ +addSql('CREATE UNIQUE INDEX person_unique ON chill_person_accompanying_period_resource (person_id, accompanyingperiod_id) WHERE person_id IS NOT NULL'); + $this->addSql('CREATE UNIQUE INDEX thirdparty_unique ON chill_person_accompanying_period_resource (thirdparty_id, accompanyingperiod_id) WHERE thirdparty_id IS NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX person_unique'); + $this->addSql('DROP INDEX thirdparty_unique'); + } +} diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20211021125359.php b/src/Bundle/ChillPersonBundle/migrations/Version20211021125359.php new file mode 100644 index 000000000..a4ed54254 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20211021125359.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE chill_person_accompanying_period_participation ADD CONSTRAINT '. + "participations_no_overlap EXCLUDE USING GIST( + -- extension btree_gist required to include comparaison with integer + person_id WITH =, accompanyingperiod_id WITH =, + daterange(startdate, enddate) WITH && + ) + INITIALLY DEFERRED"); + } + + public function down(Schema $schema): void + { + $this->addSql('CREATE UNIQUE INDEX participation_unique ON chill_person_accompanying_period_participation (accompanyingperiod_id, person_id)'); + } +} diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20211108100849.php b/src/Bundle/ChillPersonBundle/migrations/Version20211108100849.php new file mode 100644 index 000000000..49395dfae --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20211108100849.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE chill_person_person ADD civility_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_person_person ADD CONSTRAINT FK_BF210A1423D6A298 FOREIGN KEY (civility_id) REFERENCES chill_main_civility (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_BF210A1423D6A298 ON chill_person_person (civility_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_person_person DROP CONSTRAINT FK_BF210A1423D6A298'); + $this->addSql('DROP INDEX IDX_BF210A1423D6A298'); + $this->addSql('ALTER TABLE chill_person_person DROP civility_id'); + } +} diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml index ba7133b2a..e815153c8 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml @@ -26,6 +26,12 @@ household: many {Montrer # anciennes ou futures appartenances} other {Montrer # anciennes ou futures appartenances} } + Show accompanying periods of past or future memberships: >- + {length, plural, + one {Montrer les parcours d'une ancienne appartenance} + many {Montrer # parcours des anciennes ou futures appartenances} + other {Montrer # parcours des anciennes ou futures appartenances} + } Hide memberships: Masquer Those members does not share address: Ces usagers ne partagent pas l'adresse du ménage. Any persons into this position: Aucune personne n'appartient au ménage à cette position. @@ -77,6 +83,7 @@ household: Household history for %name%: Historique des ménages pour {name} Household shared: Ménages domiciliés Household not shared: Ménage non domiciliés + Members without position: Membres non positionnés Never in any household: Membre d'aucun ménage Membership currently running: En cours from: Depuis diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index a17bbc55f..ef8e58bb1 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -47,8 +47,8 @@ Phonenumber: 'Numéro de téléphone' phonenumber: numéro de téléphone Mobilenumber: 'Numéro de téléphone portable' mobilenumber: numéro de téléphone portable -Accept short text message ?: Accepte les SMS? -Accept short text message: Accepte les SMS +Accept short text message ?: La personne a donné l'autorisation d'utiliser ce no de téléphone pour l'envoi de rappel par SMS +Accept short text message: La personne a donné l'autorisation d'utiliser ce no de téléphone pour l'envoi de rappel par SMS Other phonenumber: Autre numéro de téléphone Description: description Add new phone: Ajouter un numéro de téléphone @@ -80,6 +80,8 @@ Married: Marié(e) 'Contact information': 'Informations de contact' 'Administrative information': Administratif File number: Dossier n° +Civility: Civilité +choose civility: -- # dédoublonnage Old person: Doublon @@ -403,6 +405,8 @@ Back to household: Revenir au ménage # accompanying course work Accompanying Course Actions: Actions d'accompagnements Accompanying Course Action: Action d'accompagnement +Are you sure you want to remove this work of the accompanying period %name% ?: Êtes-vous sûr de vouloir supprimer cette action de la période d'accompagnement %name% ? +The accompanying period work has been successfully removed.: L'action d'accompagnement a été supprimée. accompanying_course_work: create: Créer une action Create accompanying course work: Créer une action d'accompagnement @@ -417,6 +421,7 @@ accompanying_course_work: results: Résultats - orientations goal: Objectif - motif - dispositif Any work: Aucune action d'accompagnement + remove: Supprimer une action d'accompagnement # Person addresses: Adresses de résidence diff --git a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml index 0e77dae0c..6d2ccb3cb 100644 --- a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml @@ -41,3 +41,6 @@ household: household_membership: The end date must be after start date: La date de la fin de l'appartenance doit être postérieure à la date de début. Person with membership covering: Une personne ne peut pas appartenir à deux ménages simultanément. Or, avec cette modification, %person_name% appartiendrait à %nbHousehold% ménages à partir du %from%. + +# Accompanying period +'{{ name }} is already associated to this accompanying course.': '{{ name }} est déjà associé avec ce parcours.' \ No newline at end of file diff --git a/src/Bundle/ChillReportBundle/Entity/Report.php b/src/Bundle/ChillReportBundle/Entity/Report.php index fa81d9283..a88de85e9 100644 --- a/src/Bundle/ChillReportBundle/Entity/Report.php +++ b/src/Bundle/ChillReportBundle/Entity/Report.php @@ -1,19 +1,19 @@ - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ @@ -74,7 +74,7 @@ class Report implements HasCenterInterface, HasScopeInterface /** * @var array - * @ORM\Column(type="json_array") + * @ORM\Column(type="json") */ private $cFData; @@ -233,7 +233,7 @@ class Report implements HasCenterInterface, HasScopeInterface { return $this->cFGroup; } - + /** * @return Center */ diff --git a/src/Bundle/ChillReportBundle/Export/Export/ReportList.php b/src/Bundle/ChillReportBundle/Export/Export/ReportList.php index 583db8f4b..9524f860a 100644 --- a/src/Bundle/ChillReportBundle/Export/Export/ReportList.php +++ b/src/Bundle/ChillReportBundle/Export/Export/ReportList.php @@ -1,6 +1,6 @@ */ @@ -38,27 +38,27 @@ class ReportList implements ListInterface, ExportElementValidatedInterface * @var CustomFieldsGroup */ protected $customfieldsGroup; - + /** * * @var TranslatableStringHelper */ protected $translatableStringHelper; - + /** * * @var TranslatorInterface */ protected $translator; - + /** * * @var CustomFieldProvider */ protected $customFieldProvider; - + protected $em; - + protected $fields = array( 'person_id', 'person_firstName', 'person_lastName', 'person_birthdate', 'person_placeOfBirth', 'person_gender', 'person_memo', 'person_email', 'person_phonenumber', @@ -67,11 +67,11 @@ class ReportList implements ListInterface, ExportElementValidatedInterface 'person_address_postcode_code', 'person_address_country_name', 'person_address_country_code', 'report_id', 'report_user', 'report_date', 'report_scope' ); - + protected $slugs = []; - + function __construct( - CustomFieldsGroup $customfieldsGroup, + CustomFieldsGroup $customfieldsGroup, TranslatableStringHelper $translatableStringHelper, TranslatorInterface $translator, CustomFieldProvider $customFieldProvider, @@ -84,18 +84,18 @@ class ReportList implements ListInterface, ExportElementValidatedInterface $this->em = $em; } - + public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder) { $choices = array_combine($this->fields, $this->fields); - + foreach ($this->getCustomFields() as $cf) { $choices - [$this->translatableStringHelper->localize($cf->getName())] - = + [$this->translatableStringHelper->localize($cf->getName())] + = $cf->getSlug(); } - + // Add a checkbox to select fields $builder->add('fields', ChoiceType::class, array( 'multiple' => true, @@ -133,7 +133,7 @@ class ReportList implements ListInterface, ExportElementValidatedInterface } ))] )); - + // add a date field for addresses $builder->add('address_date', ChillDateType::class, array( 'label' => "Address valid at this date", @@ -142,14 +142,14 @@ class ReportList implements ListInterface, ExportElementValidatedInterface 'block_name' => 'list_export_form_address_date' )); } - + public function validateForm($data, ExecutionContextInterface $context) { // get the field starting with address_ $addressFields = array_filter(function($el) { return substr($el, 0, 8) === 'address_'; }, $this->fields); - + // check if there is one field starting with address in data if (count(array_intersect($data['fields'], $addressFields)) > 0) { // if a field address is checked, the date must not be empty @@ -161,10 +161,10 @@ class ReportList implements ListInterface, ExportElementValidatedInterface } } } - + /** * Get custom fields associated with person - * + * * @return CustomField[] */ private function getCustomFields() @@ -184,7 +184,7 @@ class ReportList implements ListInterface, ExportElementValidatedInterface { return $this->translator->trans( "Generate list of report '%type%'", - [ + [ '%type%' => $this->translatableStringHelper->localize($this->customfieldsGroup->getName()) ] ); @@ -192,13 +192,12 @@ class ReportList implements ListInterface, ExportElementValidatedInterface /** * {@inheritDoc} - * + * * @param type $key * @param array $values * @param type $data - * @return type */ - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): \Closure { switch ($key) { case 'person_birthdate': @@ -206,27 +205,27 @@ class ReportList implements ListInterface, ExportElementValidatedInterface // for birthdate or report date, we have to transform the string into a date // to format the date correctly. return function($value) use ($key) { - if ($value === '_header') { - return $key === 'person_birthdate' ? 'birthdate' : 'report_date'; + if ($value === '_header') { + return $key === 'person_birthdate' ? 'birthdate' : 'report_date'; } - + if (empty($value)) { return ""; } - + if ($key === 'person_birthdate') { $date = \DateTime::createFromFormat('Y-m-d', $value); } else { $date = \DateTime::createFromFormat('Y-m-d H:i:s', $value); } // check that the creation could occurs. - if ($date === false) { + if ($date === false) { throw new \Exception(sprintf("The value %s could " . "not be converted to %s", $value, \DateTime::class)); } - - return $date->format('d-m-Y'); + + return $date->format('d-m-Y'); }; case 'report_scope': $qb = $this->em->getRepository(Scope::class) @@ -236,17 +235,18 @@ class ReportList implements ListInterface, ExportElementValidatedInterface ->where($qb->expr()->in('s.id', $values)) ; $rows = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY); - + + $scopes = []; + foreach($rows as $row) { - $scopes[$row['id']] = $this->translatableStringHelper - ->localize($row['name']); + $scopes[$row['id']] = $this->translatableStringHelper->localize($row['name']); } - - return function($value) use ($scopes) { + + return function($value) use ($scopes): string { if ($value === '_header') { return 'circle'; } - + return $scopes[$value]; }; case 'report_user': @@ -257,71 +257,73 @@ class ReportList implements ListInterface, ExportElementValidatedInterface ->where($qb->expr()->in('u.id', $values)) ; $rows = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY); - + + $users = []; + foreach($rows as $row) { $users[$row['id']] = $row['username']; } - - return function($value) use ($users) { + + return function($value) use ($users): string { if ($value === '_header') { return 'user'; } - + return $users[$value]; }; case 'person_gender' : // for gender, we have to translate men/women statement return function($value) { if ($value === '_header') { return 'gender'; } - + return $this->translator->trans($value); }; case 'person_countryOfBirth': case 'person_nationality': $countryRepository = $this->em ->getRepository('ChillMainBundle:Country'); - + // load all countries in a single query $countryRepository->findBy(array('countryCode' => $values)); - + return function($value) use ($key, $countryRepository) { if ($value === '_header') { return \strtolower($key); } - - if ($value === NULL) { + + if ($value === NULL) { return $this->translator->trans('no data'); } - + $country = $countryRepository->find($value); - + return $this->translatableStringHelper->localize( $country->getName()); }; case 'person_address_country_name': return function($value) use ($key) { if ($value === '_header') { return \strtolower($key); } - + if ($value === NULL) { return ''; } - + return $this->translatableStringHelper->localize(json_decode($value, true)); }; default: // for fields which are associated with person if (in_array($key, $this->fields)) { return function($value) use ($key) { - if ($value === '_header') { return \strtolower($key); } + if ($value === '_header') { return \strtolower($key); } - return $value; + return $value; }; } else { return $this->getLabelForCustomField($key, $values, $data); } } - + } - + private function getLabelForCustomField($key, array $values, $data) { // for fields which are custom fields @@ -329,38 +331,38 @@ class ReportList implements ListInterface, ExportElementValidatedInterface $cf = $this->em ->getRepository(CustomField::class) ->findOneBy(array('slug' => $this->DQLToSlug($key))); - + $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); $defaultFunction = function($value) use ($cf) { if ($value === '_header') { - return $this->translatableStringHelper->localize($cf->getName()); + return $this->translatableStringHelper->localize($cf->getName()); } return $this->customFieldProvider ->getCustomFieldByType($cf->getType()) ->render(json_decode($value, true), $cf, 'csv'); }; - + if ($cfType instanceof CustomFieldChoice and $cfType->isMultiple($cf)) { return function($value) use ($cf, $cfType, $key) { $slugChoice = $this->extractInfosFromSlug($key)['additionnalInfos']['choiceSlug']; $decoded = \json_decode($value, true); - + if ($value === '_header') { - + $label = $cfType->getChoices($cf)[$slugChoice]; - + return $this->translatableStringHelper->localize($cf->getName()) .' | '.$label; } - + if ($slugChoice === '_other' and $cfType->isChecked($cf, $choiceSlug, $decoded)) { return $cfType->extractOtherValue($cf, $decoded); } else { return $cfType->isChecked($cf, $slugChoice, $decoded); } }; - + } else { return $defaultFunction; } @@ -369,44 +371,44 @@ class ReportList implements ListInterface, ExportElementValidatedInterface public function getQueryKeys($data) { $fields = array(); - + foreach ($data['fields'] as $key) { if (in_array($key, $this->fields)) { $fields[] = $key; } } - + // add the key from slugs and return return \array_merge($fields, \array_keys($this->slugs)); } - + /** * clean a slug to be usable by DQL - * - * @param string $slugsanitize + * + * @param string $slugsanitize * @param string $type the type of the customfield, if required (currently only for choices) * @return string */ private function slugToDQL($slug, $type = "default", array $additionalInfos = []) { $uid = 'slug_'.\uniqid(); - + $this->slugs[$uid] = [ 'slug' => $slug, 'type' => $type, 'additionnalInfos' => $additionalInfos ]; - + return $uid; } - + private function DQLToSlug($cleanedSlug) - { + { return $this->slugs[$cleanedSlug]['slug']; } - + /** - * + * * @param type $cleanedSlug * @return an array with keys = 'slug', 'type', 'additionnalInfo' */ @@ -424,7 +426,7 @@ class ReportList implements ListInterface, ExportElementValidatedInterface { return $this->translator->trans( "List for report '%type%'", - [ + [ '%type%' => $this->translatableStringHelper->localize($this->customfieldsGroup->getName()) ] ); @@ -438,22 +440,22 @@ class ReportList implements ListInterface, ExportElementValidatedInterface public function initiateQuery(array $requiredModifiers, array $acl, array $data = array()) { $centers = array_map(function($el) { return $el['center']; }, $acl); - + // throw an error if any fields are present if (!\array_key_exists('fields', $data)) { throw new \Doctrine\DBAL\Exception\InvalidArgumentException("any fields " . "have been checked"); } - + $qb = $this->em->createQueryBuilder(); - + // process fields which are not custom fields foreach ($this->fields as $f) { // do not add fields which are not selected if (!\in_array($f, $data['fields'])) { continue; } - + // add a column to the query for each field switch ($f) { case 'person_countryOfBirth': @@ -470,11 +472,11 @@ class ReportList implements ListInterface, ExportElementValidatedInterface case 'person_address_country_code': // remove 'person_' $suffix = \substr($f, 7); - + $qb->addSelect(sprintf( 'GET_PERSON_ADDRESS_%s(person.id, :address_date) AS %s', // get the part after address_ - strtoupper(substr($suffix, 8)), + strtoupper(substr($suffix, 8)), $f)); $qb->setParameter('address_date', $data['address_date']); break; @@ -487,7 +489,7 @@ class ReportList implements ListInterface, ExportElementValidatedInterface default: $prefix = \substr($f, 0, 7); $suffix = \substr($f, 7); - + switch($prefix) { case 'person_': $qb->addSelect(sprintf('person.%s as %s', $suffix, $f)); @@ -502,22 +504,22 @@ class ReportList implements ListInterface, ExportElementValidatedInterface } } - + // process fields which are custom fields foreach ($this->getCustomFields() as $cf) { // do not add custom fields which are not selected if (!\in_array($cf->getSlug(), $data['fields'])) { continue; } - + $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); - + // if is multiple, split into multiple columns if ($cfType instanceof CustomFieldChoice and $cfType->isMultiple($cf)) { foreach($cfType->getChoices($cf) as $choiceSlug => $label) { $slug = $this->slugToDQL($cf->getSlug(), 'choice', [ 'choiceSlug' => $choiceSlug ]); $qb->addSelect( - sprintf('GET_JSON_FIELD_BY_KEY(report.cFData, :slug%s) AS %s', + sprintf('GET_JSON_FIELD_BY_KEY(report.cFData, :slug%s) AS %s', $slug, $slug)); $qb->setParameter(sprintf('slug%s', $slug), $cf->getSlug()); } @@ -525,12 +527,12 @@ class ReportList implements ListInterface, ExportElementValidatedInterface // not multiple, add a single column $slug = $this->slugToDQL($cf->getSlug()); $qb->addSelect( - sprintf('GET_JSON_FIELD_BY_KEY(report.cFData, :slug%s) AS %s', + sprintf('GET_JSON_FIELD_BY_KEY(report.cFData, :slug%s) AS %s', $slug, $slug)); $qb->setParameter(sprintf('slug%s', $slug), $cf->getSlug()); } } - + $qb ->from(Report::class, 'report') ->leftJoin('report.person', 'person') @@ -540,7 +542,7 @@ class ReportList implements ListInterface, ExportElementValidatedInterface ->andWhere('center IN (:authorized_centers)') ->setParameter('authorized_centers', $centers); ; - + return $qb; } diff --git a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php index bc76dacd4..d8944ae93 100644 --- a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php +++ b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php @@ -2,11 +2,17 @@ namespace Chill\TaskBundle\Controller; +use Chill\MainBundle\Entity\Scope; +use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher; +use Chill\MainBundle\Security\Resolver\CenterResolverInterface; +use Chill\MainBundle\Templating\Listing\FilterOrderHelper; +use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; use Chill\PersonBundle\Privacy\PrivacyEvent; +use Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; -use Doctrine\ORM\EntityManager; use Chill\PersonBundle\Entity\Person; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -22,97 +28,129 @@ use Chill\TaskBundle\Repository\SingleTaskRepository; use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Repository\PersonRepository; -use Chill\MainBundle\Entity\UserRepository; use Chill\TaskBundle\Event\TaskEvent; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Translation\TranslatorInterface; use Chill\TaskBundle\Event\UI\UIEvent; use Chill\MainBundle\Repository\CenterRepository; use Chill\MainBundle\Timeline\TimelineBuilder; +use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface as TranslationTranslatorInterface; /** * Class SingleTaskController * * @package Chill\TaskBundle\Controller */ -class SingleTaskController extends AbstractController +final class SingleTaskController extends AbstractController { - - /** - * @var EventDispatcherInterface - */ - protected $eventDispatcher; - - /** - * - * @var TimelineBuilder - */ - protected $timelineBuilder; - - /** - * @var LoggerInterface - */ - protected $logger; - - /** - * SingleTaskController constructor. - * - * @param EventDispatcherInterface $eventDispatcher - */ + + private EventDispatcherInterface $eventDispatcher; + private TimelineBuilder $timelineBuilder; + private LoggerInterface $logger; + private CenterResolverDispatcher $centerResolverDispatcher; + private TranslatorInterface $translator; + private PaginatorFactory $paginatorFactory; + private SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository; + private FilterOrderHelperFactoryInterface $filterOrderHelperFactory; + public function __construct( + CenterResolverDispatcher $centerResolverDispatcher, + PaginatorFactory $paginatorFactory, + SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository, + TranslatorInterface $translator, EventDispatcherInterface $eventDispatcher, TimelineBuilder $timelineBuilder, - LoggerInterface $logger + LoggerInterface $logger, + FilterOrderHelperFactoryInterface $filterOrderHelperFactory ) { $this->eventDispatcher = $eventDispatcher; $this->timelineBuilder = $timelineBuilder; $this->logger = $logger; + $this->translator = $translator; + $this->centerResolverDispatcher = $centerResolverDispatcher; + $this->paginatorFactory = $paginatorFactory; + $this->singleTaskAclAwareRepository = $singleTaskAclAwareRepository; + $this->filterOrderHelperFactory = $filterOrderHelperFactory; } - - + + private function getEntityContext(Request $request) + { + if ($request->query->has('person_id')) { + return 'person'; + } else if ($request->query->has('course_id')) { + return 'course'; + } else { + return null; + } + } + + /** * @Route( * "/{_locale}/task/single-task/new", * name="chill_task_single_task_new" * ) */ - public function newAction( - Request $request, - TranslatorInterface $translator - ) { + public function newAction(Request $request) { $task = (new SingleTask()) ->setAssignee($this->getUser()) ->setType('task_default') ; - if ($request->query->has('person_id')) { - - $personId = $request->query->getInt('person_id', 0); // sf4 check: - // prevent error: `Argument 2 passed to ::getInt() must be of the type int, null given` + $entityType = $this->getEntityContext($request); - if ($personId === null) { - return new Response("You must provide a person_id", Response::HTTP_BAD_REQUEST); - } - - $person = $this->getDoctrine()->getManager() - ->getRepository(Person::class) - ->find($personId); - - if ($person === null) { - $this->createNotFoundException("Invalid person id"); - } - - $task->setPerson($person); + if (NULL === $entityType) { + throw new BadRequestHttpException("You must provide a entity_type"); } - $this->denyAccessUnlessGranted(TaskVoter::CREATE, $task, 'You are not ' - . 'allowed to create this task'); + $entityId = $request->query->getInt("{$entityType}_id", 0); - $form = $this->setCreateForm($task, new Role(TaskVoter::CREATE)); + if ($entityId === null) { + return new BadRequestHttpException("You must provide a {$entityType}_id"); + } + + switch ($entityType) { + case 'person': + $person = $this->getDoctrine()->getManager() + ->getRepository(Person::class) + ->find($entityId); + + if ($person === null) { + $this->createNotFoundException("Invalid person id"); + } + + $task->setPerson($person); + $role = TaskVoter::CREATE_PERSON; + break; + case 'course': + $course = $this->getDoctrine()->getManager() + ->getRepository(AccompanyingPeriod::class) + ->find($entityId); + + if ($course === null) { + $this->createNotFoundException("Invalid accompanying course id"); + } + + $task->setCourse($course); + $role = TaskVoter::CREATE_COURSE; + break; + default: + return new BadRequestHttpException("context with {$entityType} is not supported"); + } + + $this->denyAccessUnlessGranted($role, $task, 'You are not ' + . 'allowed to create this task'); + + $form = $this->setCreateForm($task, new Role($role)); $form->handleRequest($request); - + if ($form->isSubmitted()) { if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); @@ -122,72 +160,78 @@ class SingleTaskController extends AbstractController $em->flush(); - $this->addFlash('success', $translator->trans("The task is created")); + $this->addFlash('success', $this->translator->trans("The task is created")); - return $this->redirectToRoute('chill_task_singletask_list', [ - 'person_id' => $task->getPerson()->getId() - ]); + if ($request->query->has('returnPath')) { + return $this->redirect($request->query->get('returnPath')); + } + if ($entityType === 'person') { + return $this->redirectToRoute('chill_task_singletask_by-person_list', [ + 'id' => $task->getPerson()->getId() + ]); + } elseif ($entityType === 'course') { + return $this->redirectToRoute('chill_task_singletask_by-course_list', [ + 'id' => $task->getCourse()->getId() + ]); + } } else { - $this->addFlash('error', $translator->trans("This form contains errors")); + $this->addFlash('error', $this->translator->trans("This form contains errors")); } } - return $this->render('ChillTaskBundle:SingleTask:new.html.twig', array( - 'form' => $form->createView(), - 'task' => $task - )); + switch ($entityType) { + case 'person': + return $this->render('@ChillTask/SingleTask/Person/new.html.twig', array( + 'form' => $form->createView(), + 'task' => $task, + 'person' => $task->getPerson(), + )); + case 'course': + return $this->render('@ChillTask/SingleTask/AccompanyingCourse/new.html.twig', array( + 'form' => $form->createView(), + 'task' => $task, + 'accompanyingCourse' => $task->getCourse(), + )); + default: + throw new \LogicException("entity context not supported"); + } } - /** * @Route( * "/{_locale}/task/single-task/{id}/show", * name="chill_task_single_task_show" * ) */ - public function showAction(Request $request, $id) + public function showAction(SingleTask $task, Request $request) { - - $em = $this->getDoctrine()->getManager(); - $task = $em->getRepository(SingleTask::class)->find($id); + $this->denyAccessUnlessGranted(TaskVoter::SHOW, $task); - if ($task->getPerson() !== null) { - $personId = $task->getPerson()->getId(); - - if ($personId === null) { - return new Response("You must provide a person_id", Response::HTTP_BAD_REQUEST); - } - - $person = $this->getDoctrine()->getManager() - ->getRepository(Person::class) - ->find($personId); - - if ($person === null) { - throw $this->createNotFoundException("Invalid person id"); - } - } - $this->denyAccessUnlessGranted(TaskVoter::SHOW, $task, 'You are not ' - . 'allowed to view this task'); - - if (!$task) { - throw $this->createNotFoundException('Unable to find Task entity.'); + if ($person = $task->getContext() instanceof Person) { + $event = new PrivacyEvent($person, array( + 'element_class' => SingleTask::class, + 'element_id' => $task->getId(), + 'action' => 'show' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); } $timeline = $this->timelineBuilder ->getTimelineHTML('task', array('task' => $task)); - - $event = new PrivacyEvent($person, array( - 'element_class' => SingleTask::class, - 'element_id' => $task->getId(), - 'action' => 'show' - )); - $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); - - return $this->render('ChillTaskBundle:SingleTask:show.html.twig', array( - 'task' => $task, - 'timeline' => $timeline - )); + + if ($task->getContext() instanceof Person) { + return $this->render('@ChillTask/SingleTask/Person/show.html.twig', array( + 'task' => $task, + 'timeline' => $timeline + )); + } else { + return $this->render('@ChillTask/SingleTask/AccompanyingCourse/show.html.twig', array( + 'task' => $task, + 'timeline' => $timeline + )); + } + } @@ -198,41 +242,18 @@ class SingleTaskController extends AbstractController * ) */ public function editAction( - Request $request, - $id, - TranslatorInterface $translator + SingleTask $task, + Request $request ) { - - $em = $this->getDoctrine()->getManager(); - $task = $em->getRepository(SingleTask::class)->find($id); - if ($task->getPerson() !== null) { - $personId = $task->getPerson()->getId(); - if ($personId === null) { - return new Response("You must provide a person_id", Response::HTTP_BAD_REQUEST); - } - - $person = $this->getDoctrine()->getManager() - ->getRepository(Person::class) - ->find($personId); - - if ($person === null) { - throw $this->createNotFoundException("Invalid person id"); - } - } $this->denyAccessUnlessGranted(TaskVoter::UPDATE, $task, 'You are not ' . 'allowed to edit this task'); - if (!$task) { - throw $this->createNotFoundException('Unable to find Task entity.'); - } - $event = (new UIEvent('single-task', $task)) ->setForm($this->setCreateForm($task, new Role(TaskVoter::UPDATE))) ; - $this->eventDispatcher->dispatch(UIEvent::EDIT_FORM, $event); - + $form = $event->getForm(); $form->handleRequest($request); @@ -244,43 +265,64 @@ class SingleTaskController extends AbstractController $em->flush(); - $this->addFlash('success', $translator + $this->addFlash('success', $this->translator ->trans("The task has been updated")); - - $event = new PrivacyEvent($person, array( + + if ($task->getContext() instanceof Person) { + $event = new PrivacyEvent($task->getPerson(), array( 'element_class' => SingleTask::class, 'element_id' => $task->getId(), 'action' => 'update' - )); - $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); - return $this->redirectToRoute( - 'chill_task_singletask_list', - $request->query->get('list_params', []) - ); + if ($request->query->has('returnPath')) { + return $this->redirect($request->query->get('returnPath')); + } + return $this->redirectToRoute( + 'chill_task_singletask_list', + ); + } else { + if ($request->query->has('returnPath')) { + return $this->redirect($request->query->get('returnPath')); + } + + return $this->redirectToRoute( + 'chill_task_singletask_by-course_list', ['id' => $task->getCourse()->getId()] + ); + } } else { - $this->addFlash('error', $translator->trans("This form contains errors")); + $this->addFlash('error', $this->translator->trans("This form contains errors")); } } - + $this->eventDispatcher->dispatch(UIEvent::EDIT_PAGE, $event); - + if ($event->hasResponse()) { return $event->getResponse(); } - - $event = new PrivacyEvent($person, array( - 'element_class' => SingleTask::class, - 'element_id' => $task->getId(), - 'action' => 'edit' - )); - $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); - - return $this->render('ChillTaskBundle:SingleTask:edit.html.twig', array( - 'task' => $task, - 'form' => $form->createView() - )); + + if ($task->getContext() instanceof Person) { + $event = new PrivacyEvent($task->getPerson(), array( + 'element_class' => SingleTask::class, + 'element_id' => $task->getId(), + 'action' => 'edit' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->render('@ChillTask/SingleTask/Person/edit.html.twig', array( + 'task' => $task, + 'form' => $form->createView() + )); + } else { + return $this->render('@ChillTask/SingleTask/AccompanyingCourse/edit.html.twig', array( + 'task' => $task, + 'form' => $form->createView(), + 'accompanyingCourse' => $task->getCourse() + )); + } + } @@ -292,10 +334,9 @@ class SingleTaskController extends AbstractController */ public function deleteAction( Request $request, - $id, - TranslatorInterface $translator + $id ) { - + $em = $this->getDoctrine()->getManager(); $task = $em->getRepository(SingleTask::class)->find($id); @@ -317,10 +358,25 @@ class SingleTaskController extends AbstractController throw $this->createNotFoundException("Invalid person id"); } + } else { + $courseId = $task->getCourse()->getId(); + if ($courseId === null){ + return new Response("You must provide a course_id", Response::HTTP_BAD_REQUEST); + } + + $course = $this->getDoctrine()->getManager() + ->getRepository(AccompanyingPeriod::class) + ->find($courseId); + + if($course === null){ + throw $this->createNotFoundException("Invalid accompanying period id"); + } } - $this->denyAccessUnlessGranted(TaskVoter::DELETE, $task, 'You are not ' - . 'allowed to delete this task'); + // TODO: reactivate right to delete + + // $this->denyAccessUnlessGranted(TaskVoter::DELETE, $task, 'You are not ' + // . 'allowed to delete this task'); $form = $this->createDeleteForm($id); @@ -328,38 +384,50 @@ class SingleTaskController extends AbstractController $form->handleRequest($request); if ($form->isValid()) { - + $this->logger->notice("A task has been removed", array( 'by_user' => $this->getUser()->getUsername(), 'task_id' => $task->getId(), 'description' => $task->getDescription(), 'assignee' => $task->getAssignee(), - 'scope_id' => $task->getScope()->getId(), - //'start_date' => $task->getStartDate()->format('Y-m-d'), - //'end_date' => $task->getEndDate()->format('Y-m-d'), - //'warning_interval' => $task->getWarningInterval()->format('Y-m-d') + // TODO reimplement scope + // 'scope_id' => $task->getScope()->getId(), )); $em = $this->getDoctrine()->getManager(); $em->remove($task); $em->flush(); - $this->addFlash('success', $translator + $this->addFlash('success', $this->translator ->trans("The task has been successfully removed.")); - return $this->redirect($this->generateUrl( - 'chill_task_singletask_list', - $request->query->get('list_params', [ - 'person_id' => $person->getId() - ]))); + if ($task->getContext() instanceof Person) { + return $this->redirect($this->generateUrl( + 'chill_task_singletask_by-person_list', + [ 'id' => $task->getPerson()->getId() ] + )); + } else { + return $this->redirect($this->generateUrl( + 'chill_task_singletask_by-course_list', + ['id' => $task->getCourse()->getId()] + )); + } } } + if($task->getContext() instanceof Person){ + return $this->render('@ChillTask/SingleTask/Person/confirm_delete.html.twig', array( + 'task' => $task, + 'delete_form' => $form->createView() + )); + } else { + return $this->render('@ChillTask/SingleTask/AccompanyingCourse/confirm_delete.html.twig', array( + 'task' => $task, + 'delete_form' => $form->createView(), + 'accompanyingCourse' => $course + )); + } - return $this->render('ChillTaskBundle:SingleTask:confirm_delete.html.twig', array( - 'task' => $task, - 'delete_form' => $form->createView() - )); } /** @@ -371,8 +439,7 @@ class SingleTaskController extends AbstractController protected function setCreateForm(SingleTask $task, Role $role) { $form = $this->createForm(SingleTaskType::class, $task, [ - 'center' => $task->getCenter(), - 'role' => $role + 'role' => $role, ]); $form->add('submit', SubmitType::class); @@ -385,223 +452,114 @@ class SingleTaskController extends AbstractController * @return Response * @Route( * "/{_locale}/task/single-task/list/my", - * name="chill_task_single_my_tasks" + * name="chill_task_singletask_my_tasks" * ) */ - public function myTasksAction(TranslatorInterface $translator) + public function myTasksAction() { - return $this->redirectToRoute('chill_task_singletask_list', [ - 'user_id' => $this->getUser()->getId(), - 'hide_form' => true, - 'title' => $translator->trans('My tasks') + $this->denyAccessUnlessGranted('ROLE_USER'); + + $filterOrder = $this->buildFilterOrder(); + $flags = \array_merge( + $filterOrder->getCheckboxData('status'), + \array_map(fn ($i) => 'state_'.$i, $filterOrder->getCheckboxData('states')) + ); + $nb = $this->singleTaskAclAwareRepository->countByCurrentUsersTasks( + $filterOrder->getQueryString(), + $flags + ); + $paginator = $this->paginatorFactory->create($nb); + $tasks = $this->singleTaskAclAwareRepository->findByCurrentUsersTasks( + $filterOrder->getQueryString(), + $flags, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage(), + [ + 'startDate' => 'DESC', + 'endDate' => 'DESC', + ] + ); + + return $this->render('@ChillTask/SingleTask/List/index_my_tasks.html.twig', [ + 'tasks' => $tasks, + 'paginator' => $paginator, + 'filter_order' => $filterOrder, ]); } + private function buildFilterOrder(): FilterOrderHelper + { + $statuses = ['no-alert', 'warning', 'alert']; + $statusTrans = [ + 'Tasks without alert', + 'Tasks near deadline', + 'Tasks over deadline', + ]; + $states = [ + // todo: get a list of possible states dynamically + 'new', 'in_progress', 'closed', 'canceled' + ]; + return $this->filterOrderHelperFactory + ->create(self::class) + ->addSearchBox() + ->addCheckbox('status', $statuses, $statuses, $statusTrans) + ->addCheckbox('states', $states, ['new', 'in_progress']) + ->build() + ; + } + /** * * Arguments: * - user_id * - scope_id + * - s * - person_id * - hide_form (hide the form to filter the tasks) * - status: date state, amongst SingleTaskRepository::DATE_STATUSES, or 'closed' * * @Route( - * "/{_locale}/task/singletask/list", + * "/{_locale}/task/single-task/list", * name="chill_task_singletask_list" * ) */ public function listAction( - Request $request, - PaginatorFactory $paginatorFactory, - SingleTaskRepository $taskRepository, - PersonRepository $personRepository, - CenterRepository $centerRepository, - FormFactoryInterface $formFactory + Request $request ) { - /* @var $viewParams array The parameters for the view */ - /* @var $params array The parameters for the query */ + $this->denyAccessUnlessGranted(TaskVoter::SHOW, null); - $viewParams['person'] = null; - $params['person'] = null; - $viewParams['user'] = null; - $params['user'] = null; - $viewParams['center'] = null; - $params['types'] = null; + $filterOrder = $this->buildFilterOrder(); + $flags = \array_merge( + $filterOrder->getCheckboxData('status'), + \array_map(fn ($i) => 'state_'.$i, $filterOrder->getCheckboxData('states')) + ); + $nb = $this->singleTaskAclAwareRepository->countByAllViewable( + $filterOrder->getQueryString(), + $flags + ); + $paginator = $this->paginatorFactory->create($nb); - - // Get parameters from url - if (!empty($request->query->get('person_id', NULL))) { - - $personId = $request->query->getInt('person_id', 0); - $person = $personRepository->find($personId); - - if ($person === null) { - throw $this->createNotFoundException("This person ' $personId ' does not exist."); - } - $this->denyAccessUnlessGranted(PersonVoter::SEE, $person); - - $viewParams['person'] = $person; - $params['person'] = $person; - } - - if (!empty($request->query->get('center_id', NULL))) { - $center = $centerRepository->find($request->query->getInt('center_id')); - if ($center === null) { - throw $this->createNotFoundException('center not found'); - } - $params['center'] = $center; - } - - if(!empty($request->query->get('types', []))) { - $types = $request->query->get('types', []); - if (count($types) > 0) { - $params['types'] = $types; - } - } - - if (!empty($request->query->get('user_id', null))) { - if ($request->query->get('user_id') === '_unassigned') { - $params['unassigned'] = true; - } else { - - $userId = $request->query->getInt('user_id', 0); - $user = $this->getDoctrine()->getManager() - ->getRepository('ChillMainBundle:User') - ->find($userId); - - if ($user === null) { - throw $this->createNotFoundException("This user ' $userId ' does not exist."); - } - - $viewParams['user'] = $user; - $params['user'] = $user; - } - } - - if (!empty($request->query->get('scope_id'))) { - - $scopeId = $request->query->getInt('scope_id', 0); - - $scope = $this->getDoctrine()->getManager() - ->getRepository('ChillMainBundle:Scope') - ->find($scopeId); - - if ($scope === null) { - throw $this->createNotFoundException("This scope' $scopeId 'does not exist."); - } - - $viewParams['scope'] = $scope; - $params['scope'] = $scope; - } - - // collect parameters for filter - $possibleStatuses = \array_merge(SingleTaskRepository::DATE_STATUSES, [ 'closed' ]); - $statuses = $request->query->get('status', $possibleStatuses); - - // check for invalid statuses - $diff = \array_diff($statuses, $possibleStatuses); - if (count($diff) > 0) { - return new Response( - 'date_status not allowed: '. \implode(', ', $diff), - Response::HTTP_BAD_REQUEST - ); - } - - $viewParams['isSingleStatus'] = $singleStatus = count($statuses) === 1; - - $tasks_count = 0; - - foreach($statuses as $status) { - if($request->query->has('status') - && FALSE === \in_array($status, $statuses)) { - continue; - } - - // different query if regarding to date or 'closed' - if (in_array($status, SingleTaskRepository::DATE_STATUSES)) { - $params['date_status'] = $status; - $params['is_closed'] = false; - } else { - $params['date_status'] = null; - $params['is_closed'] = true; - } - - $count = $taskRepository - ->countByParameters($params, $this->getUser()) - ; - $paginator = $paginatorFactory->create($count); - - $viewParams['single_task_'.$status.'_count'] = $count; - $viewParams['single_task_'.$status.'_paginator'] = $paginator; - $viewParams['single_task_'.$status.'_tasks'] = $taskRepository - ->findByParameters($params, $this->getUser(), - $singleStatus ? $paginator->getCurrentPage()->getFirstItemNumber() : 0, - $singleStatus ? $paginator->getItemsPerPage() : 10) - ; - - $tasks_count = $tasks_count + $count; - } - - // total number of tasks - $viewParams['tasks_count'] = $tasks_count; - - if ($viewParams['person'] !== null){ - $viewParams['layout'] = '@ChillPerson/Person/layout.html.twig'; + if (0 < $nb) { + $tasks = $this->singleTaskAclAwareRepository->findByAllViewable( + $filterOrder->getQueryString(), + $flags, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage(), + [ + 'startDate' => 'DESC', + 'endDate' => 'DESC', + ] + ); } else { - $viewParams['layout'] = '@ChillMain/layout.html.twig'; + $tasks = []; } - // Form for filtering tasks - $form = $formFactory->createNamed(null, SingleTaskListType::class, null, [ - 'person' => $viewParams['person'], - 'method' => Request::METHOD_GET, - 'csrf_protection' => false, - 'add_type' => true - ]); + return $this->render('@ChillTask/SingleTask/List/index.html.twig', [ + 'tasks' => $tasks, + 'paginator' => $paginator, + 'filter_order' => $filterOrder + ]); - $form->handleRequest($request); - - if (isset($person)) { - $event = new PrivacyEvent($person, array( - 'element_class' => SingleTask::class, - 'action' => 'list' - )); - $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); - } - - return $this->render('ChillTaskBundle:SingleTask:index.html.twig', - \array_merge($viewParams, [ 'form' => $form->createView() ])); - } - - - protected function getPersonParam(Request $request, EntityManagerInterface $em) - { - $person = $em->getRepository(Person::class) - ->find($request->query->getInt('person_id')) - ; - - if (NULL === $person) { - throw $this->createNotFoundException('person not found'); - } - - $this->denyAccessUnlessGranted(PersonVoter::SEE, $person, "You are " - . "not allowed to see this person"); - - return $person; - } - - protected function getUserParam(Request $request, EntityManagerInterface $em) - { - $user = $em->getRepository(User::class) - ->find($request->query->getInt('user_id')) - ; - - if (NULL === $user) { - throw $this->createNotFoundException('user not found'); - } - - return $user; } /** @@ -623,4 +581,103 @@ class SingleTaskController extends AbstractController ; } + /** + * @Route( + * "/{_locale}/task/single-task/by-course/{id}", + * name="chill_task_singletask_by-course_list") + */ + public function listCourseTasks( + AccompanyingPeriod $course, + FormFactoryInterface $formFactory, + Request $request + ): Response + { + $this->denyAccessUnlessGranted(TaskVoter::SHOW, $course); + + $filterOrder = $this->buildFilterOrder(); + $flags = \array_merge( + $filterOrder->getCheckboxData('status'), + \array_map(fn ($i) => 'state_'.$i, $filterOrder->getCheckboxData('states')) + ); + $nb = $this->singleTaskAclAwareRepository->countByCourse( + $course, + $filterOrder->getQueryString(), + $flags + ); + $paginator = $this->paginatorFactory->create($nb); + + if (0 < $nb) { + $tasks = $this->singleTaskAclAwareRepository->findByCourse( + $course, + $filterOrder->getQueryString(), + $flags, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage(), + [ + 'startDate' => 'DESC', + 'endDate' => 'DESC', + ] + ); + } else { + $tasks = []; + } + + return $this->render( + '@ChillTask/SingleTask/AccompanyingCourse/list.html.twig', + [ + 'tasks' => $tasks, + 'accompanyingCourse' => $course, + 'paginator' => $paginator, + 'filter_order' => $filterOrder + ]); + } + + /** + * @Route( + * "/{_locale}/task/single-task/by-person/{id}", + * name="chill_task_singletask_by-person_list") + */ + public function listPersonTasks( + Person $person + ): Response { + + $this->denyAccessUnlessGranted(TaskVoter::SHOW, $person); + + $filterOrder = $this->buildFilterOrder(); + $flags = \array_merge( + $filterOrder->getCheckboxData('status'), + \array_map(fn ($i) => 'state_'.$i, $filterOrder->getCheckboxData('states')) + ); + $nb = $this->singleTaskAclAwareRepository->countByPerson( + $person, + $filterOrder->getQueryString(), + $flags + ); + $paginator = $this->paginatorFactory->create($nb); + + if (0 < $nb) { + $tasks = $this->singleTaskAclAwareRepository->findByPerson( + $person, + $filterOrder->getQueryString(), + $flags, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage(), + [ + 'startDate' => 'DESC', + 'endDate' => 'DESC', + ] + ); + } else { + $tasks = []; + } + + return $this->render( + '@ChillTask/SingleTask/Person/list.html.twig', + [ + 'tasks' => $tasks, + 'person' => $person, + 'paginator' => $paginator, + 'filter_order' => $filterOrder + ]); + } } diff --git a/src/Bundle/ChillTaskBundle/Controller/TaskController.php b/src/Bundle/ChillTaskBundle/Controller/TaskController.php index 0c2b1f8d2..c137c166d 100644 --- a/src/Bundle/ChillTaskBundle/Controller/TaskController.php +++ b/src/Bundle/ChillTaskBundle/Controller/TaskController.php @@ -64,7 +64,7 @@ class TaskController extends AbstractController 'id' => $task->getId(), 'list_params' => $request->query->get('list_params', []) ]); - $defaultTemplate = '@ChillTask/SingleTask/transition.html.twig'; + $task->getCourse() === null ? $defaultTemplate = '@ChillTask/SingleTask/Person/transition.html.twig' : $defaultTemplate = '@ChillTask/SingleTask/AccompanyingCourse/transition.html.twig'; break; default: return new Response("The type '$kind' is not implemented", diff --git a/src/Bundle/ChillTaskBundle/DataFixtures/ORM/LoadTaskACL.php b/src/Bundle/ChillTaskBundle/DataFixtures/ORM/LoadTaskACL.php index 5af11f6a2..513d18ea8 100644 --- a/src/Bundle/ChillTaskBundle/DataFixtures/ORM/LoadTaskACL.php +++ b/src/Bundle/ChillTaskBundle/DataFixtures/ORM/LoadTaskACL.php @@ -40,7 +40,7 @@ class LoadTaskACL extends AbstractFixture implements OrderedFixtureInterface return 16000; } - + public function load(ObjectManager $manager) { foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) { @@ -58,33 +58,38 @@ class LoadTaskACL extends AbstractFixture implements OrderedFixtureInterface case 'direction': if (in_array($scope->getName()['en'], array('administrative', 'social'))) { break 2; // we do not want any power on social or administrative - } + } break; } - + printf("Adding CHILL_TASK_TASK_UPDATE & CHILL_TASK_TASK_CREATE & Chill_TASK_TASK_DELETE permissions to %s " - . "permission group, scope '%s' \n", + . "permission group, scope '%s' \n", $permissionsGroup->getName(), $scope->getName()['en']); $roleScopeUpdate = (new RoleScope()) ->setRole(TaskVoter::UPDATE) ->setScope($scope); $permissionsGroup->addRoleScope($roleScopeUpdate); - $roleScopeCreate = (new RoleScope()) - ->setRole(TaskVoter::CREATE) + $roleScopeCreateP = (new RoleScope()) + ->setRole(TaskVoter::CREATE_PERSON) ->setScope($scope); - $permissionsGroup->addRoleScope($roleScopeCreate); + $permissionsGroup->addRoleScope($roleScopeCreateP); + $roleScopeCreateC = (new RoleScope()) + ->setRole(TaskVoter::CREATE_COURSE) + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeCreateC); $roleScopeDelete = (new RoleScope()) ->setRole(TaskVoter::DELETE) ->setScope($scope); $permissionsGroup->addRoleScope($roleScopeDelete); - + $manager->persist($roleScopeUpdate); - $manager->persist($roleScopeCreate); + $manager->persist($roleScopeCreateP); + $manager->persist($roleScopeCreateC); $manager->persist($roleScopeDelete); } - + } - + $manager->flush(); } diff --git a/src/Bundle/ChillTaskBundle/DependencyInjection/ChillTaskExtension.php b/src/Bundle/ChillTaskBundle/DependencyInjection/ChillTaskExtension.php index 5ffbd38e7..ed2a36595 100644 --- a/src/Bundle/ChillTaskBundle/DependencyInjection/ChillTaskExtension.php +++ b/src/Bundle/ChillTaskBundle/DependencyInjection/ChillTaskExtension.php @@ -44,7 +44,7 @@ class ChillTaskExtension extends Extension implements PrependExtensionInterface $this->prependRoute($container); $this->prependWorkflows($container); } - + protected function prependRoute(ContainerBuilder $container) { //declare routes for task bundle @@ -56,17 +56,18 @@ class ChillTaskExtension extends Extension implements PrependExtensionInterface ) )); } - + protected function prependAuthorization(ContainerBuilder $container) { $container->prependExtensionConfig('security', array( 'role_hierarchy' => array( TaskVoter::UPDATE => [TaskVoter::SHOW], - TaskVoter::CREATE => [TaskVoter::SHOW] + TaskVoter::CREATE_COURSE => [TaskVoter::SHOW], + TaskVoter::CREATE_PERSON => [TaskVoter::SHOW], ) )); } - + protected function prependWorkflows(ContainerBuilder $container) { $container->prependExtensionConfig('framework', [ diff --git a/src/Bundle/ChillTaskBundle/Entity/AbstractTask.php b/src/Bundle/ChillTaskBundle/Entity/AbstractTask.php index 73e98c0dd..d34a0b05f 100644 --- a/src/Bundle/ChillTaskBundle/Entity/AbstractTask.php +++ b/src/Bundle/ChillTaskBundle/Entity/AbstractTask.php @@ -10,15 +10,12 @@ use Chill\MainBundle\Entity\HasScopeInterface; use Chill\MainBundle\Entity\HasCenterInterface; use Symfony\Component\Validator\Constraints as Assert; use Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistency; +use Chill\PersonBundle\Entity\AccompanyingPeriod; /** * AbstractTask * * @ORM\MappedSuperclass() - * @UserCircleConsistency( - * "CHILL_TASK_TASK_SHOW", - * getUserFunction="getAssignee" - * ) */ abstract class AbstractTask implements HasScopeInterface, HasCenterInterface { @@ -51,7 +48,7 @@ abstract class AbstractTask implements HasScopeInterface, HasCenterInterface * @ORM\Column(name="description", type="text") */ private $description = ''; - + /** * * @var User @@ -60,36 +57,42 @@ abstract class AbstractTask implements HasScopeInterface, HasCenterInterface * ) */ private $assignee; - + /** * * @var Person * @ORM\ManyToOne( * targetEntity="\Chill\PersonBundle\Entity\Person" * ) - * @Assert\NotNull() */ private $person; - + + + /** + * @var AccompanyingPeriod + * @ORM\ManyToOne(targetEntity="\Chill\PersonBundle\Entity\AccompanyingPeriod") + */ + + private $course; + /** * * @var Scope * @ORM\ManyToOne( * targetEntity="\Chill\MainBundle\Entity\Scope" - * ) - * @Assert\NotNull() + * ) */ private $circle; - + /** * @var boolean * @ORM\Column(name="closed", type="boolean", options={ "default"=false }) */ private $closed = false; - + public function __construct() { - + } /** @@ -118,7 +121,7 @@ abstract class AbstractTask implements HasScopeInterface, HasCenterInterface /** * Set currentStates - * + * * The current states are sorted in a single array, non associative. * * @param $currentStates @@ -134,8 +137,8 @@ abstract class AbstractTask implements HasScopeInterface, HasCenterInterface /** * Get currentStates - * - * The states are returned as required by marking store format. + * + * The states are returned as required by marking store format. * * @return array */ @@ -191,7 +194,7 @@ abstract class AbstractTask implements HasScopeInterface, HasCenterInterface { return $this->description; } - + public function getAssignee(): ?User { return $this->assignee; @@ -202,11 +205,16 @@ abstract class AbstractTask implements HasScopeInterface, HasCenterInterface return $this->person; } + public function getCourse(): ?AccompanyingPeriod + { + return $this->course; + } + public function getCircle(): ?Scope { return $this->circle; } - + public function setAssignee(User $assignee = null) { $this->assignee = $assignee; @@ -219,6 +227,12 @@ abstract class AbstractTask implements HasScopeInterface, HasCenterInterface return $this; } + public function setCourse(AccompanyingPeriod $course) + { + $this->course = $course; + return $this; + } + public function setCircle(Scope $circle) { $this->circle = $circle; @@ -229,12 +243,19 @@ abstract class AbstractTask implements HasScopeInterface, HasCenterInterface { if ($this->getPerson() instanceof Person) { return $this->getPerson()->getCenter(); + } else { + return $this->getCourse()->getCenter(); } - + return null; - + } - + + public function getContext() + { + return $this->getPerson() ?? $this->getCourse(); + } + public function getScope(): ?\Chill\MainBundle\Entity\Scope { return $this->getCircle(); @@ -247,9 +268,9 @@ abstract class AbstractTask implements HasScopeInterface, HasCenterInterface { return $this->closed; } - + /** - * + * * @param bool $closed */ public function setClosed(bool $closed) diff --git a/src/Bundle/ChillTaskBundle/Entity/SingleTask.php b/src/Bundle/ChillTaskBundle/Entity/SingleTask.php index 23bdd61df..cef59a10a 100644 --- a/src/Bundle/ChillTaskBundle/Entity/SingleTask.php +++ b/src/Bundle/ChillTaskBundle/Entity/SingleTask.php @@ -19,6 +19,10 @@ use Doctrine\Common\Collections\Collection; * @ORM\Index( * name="by_current_state", * columns={ "current_states" } + * ), + * @ORM\Index( + * name="by_end_date", + * columns={ "end_date" } * ) * } * ) @@ -40,12 +44,12 @@ class SingleTask extends AbstractTask * * @ORM\Column(name="start_date", type="date", nullable=true) * @Assert\Date() - * + * * @Assert\Expression( * "value === null or this.getEndDate() === null or value < this.getEndDate()", * message="The start date must be before the end date" * ) - * + * * @Assert\Expression( * "value === null or this.getWarningDate() === null or this.getWarningDate() > this.getStartDate()", * message="The start date must be before warning date" @@ -66,16 +70,16 @@ class SingleTask extends AbstractTask * and this.getEndDate() === null * * @ORM\Column(name="warning_interval", type="dateinterval", nullable=true) - * + * * @Assert\Expression( * "!(value != null and this.getEndDate() == null)", * message="An end date is required if a warning interval is set" * ) - * - * + * + * */ private $warningInterval; - + /** * * @var RecurringTask @@ -85,7 +89,7 @@ class SingleTask extends AbstractTask * ) */ private $recurringTask; - + /** * * @var \Doctrine\Common\Collections\Collection @@ -96,15 +100,15 @@ class SingleTask extends AbstractTask * ) */ private $taskPlaceEvents; - + public function __construct() { $this->taskPlaceEvents = $events = new \Doctrine\Common\Collections\ArrayCollection; - + parent::__construct(); } - + /** * Get id * @@ -186,13 +190,13 @@ class SingleTask extends AbstractTask { return $this->warningInterval; } - + /** - * Get the Warning date, computed from the difference between the + * Get the Warning date, computed from the difference between the * end date and the warning interval - * + * * Return null if warningDate or endDate is null - * + * * @return \DateTimeImmutable */ public function getWarningDate() @@ -200,15 +204,15 @@ class SingleTask extends AbstractTask if ($this->getWarningInterval() === null) { return null; } - + if ($this->getEndDate() === null) { return null; } - + return \DateTimeImmutable::createFromMutable($this->getEndDate()) ->sub($this->getWarningInterval()); } - + function getRecurringTask(): RecurringTask { return $this->recurringTask; @@ -227,7 +231,7 @@ class SingleTask extends AbstractTask public function setTaskPlaceEvents(Collection $taskPlaceEvents) { $this->taskPlaceEvents = $taskPlaceEvents; - + return $this; } } diff --git a/src/Bundle/ChillTaskBundle/Form/SingleTaskListType.php b/src/Bundle/ChillTaskBundle/Form/SingleTaskListType.php index 99e8ddb42..634035f96 100644 --- a/src/Bundle/ChillTaskBundle/Form/SingleTaskListType.php +++ b/src/Bundle/ChillTaskBundle/Form/SingleTaskListType.php @@ -284,6 +284,7 @@ class SingleTaskListType extends AbstractType ->setDefined('person') ->setDefault('person', null) ->setAllowedTypes('person', [Person::class, 'null']) + ->setDefined('accompanyingCourse') ->setDefined('add_status') ->setDefault('add_status', false) ->setAllowedTypes('add_status', ['bool']) diff --git a/src/Bundle/ChillTaskBundle/Form/SingleTaskType.php b/src/Bundle/ChillTaskBundle/Form/SingleTaskType.php index 16f000aa2..a3dd1777f 100644 --- a/src/Bundle/ChillTaskBundle/Form/SingleTaskType.php +++ b/src/Bundle/ChillTaskBundle/Form/SingleTaskType.php @@ -17,6 +17,10 @@ */ namespace Chill\TaskBundle\Form; +use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher; +use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher; +use Chill\TaskBundle\Security\Authorization\TaskVoter; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Chill\MainBundle\Form\Type\ChillDateType; @@ -29,15 +33,26 @@ use Symfony\Component\Security\Core\Role\Role; use Chill\MainBundle\Form\Type\DateIntervalType; use Chill\MainBundle\Form\Type\ChillTextareaType; -/** - * - * - * @author Julien Fastré - */ class SingleTaskType extends AbstractType { + private ParameterBagInterface $parameterBag; + private CenterResolverDispatcher $centerResolverDispatcher; + private ScopeResolverDispatcher $scopeResolverDispatcher; + + public function __construct(ParameterBagInterface $parameterBag, CenterResolverDispatcher $centerResolverDispatcher, ScopeResolverDispatcher $scopeResolverDispatcher) + { + $this->parameterBag = $parameterBag; + $this->centerResolverDispatcher = $centerResolverDispatcher; + $this->scopeResolverDispatcher = $scopeResolverDispatcher; + } + public function buildForm(FormBuilderInterface $builder, array $options) { + if (NULL !== $task = $options['data']) { + $center = $this->centerResolverDispatcher->resolveCenter($task); + $isScopeConcerned = $this->scopeResolverDispatcher->isConcerned($task); + } + $builder ->add('title', TextType::class) ->add('description', ChillTextareaType::class, [ @@ -45,14 +60,10 @@ class SingleTaskType extends AbstractType ]) ->add('assignee', UserPickerType::class, [ 'required' => false, - 'center' => $options['center'], - 'role' => $options['role'], + 'center' => $center, + 'role' => TaskVoter::SHOW, 'placeholder' => 'Not assigned' - ]) - ->add('circle', ScopePickerType::class, [ - 'center' => $options['center'], - 'role' => $options['role'] - ]) + ]) ->add('startDate', ChillDateType::class, [ 'required' => false ]) @@ -61,17 +72,24 @@ class SingleTaskType extends AbstractType ]) ->add('warningInterval', DateIntervalType::class, [ 'required' => false - ]) - ; + ]); + + if ($this->parameterBag->get('chill_main')['acl']['form_show_scopes'] + && $isScopeConcerned) { + $builder + ->add('circle', ScopePickerType::class, [ + 'center' => $center, + 'role' => $options['role'], + 'required' => false + ]); + } } - + public function configureOptions(OptionsResolver $resolver) { $resolver - ->setRequired('center') - ->setAllowedTypes('center', [ Center::class ]) ->setRequired('role') - ->setAllowedTypes('role', [ Role::class ]) + ->setAllowedTypes('role', [ Role::class, 'string' ]) ; } } diff --git a/src/Bundle/ChillTaskBundle/Menu/PersonMenuBuilder.php b/src/Bundle/ChillTaskBundle/Menu/MenuBuilder.php similarity index 56% rename from src/Bundle/ChillTaskBundle/Menu/PersonMenuBuilder.php rename to src/Bundle/ChillTaskBundle/Menu/MenuBuilder.php index ee0afa497..dfc95535b 100644 --- a/src/Bundle/ChillTaskBundle/Menu/PersonMenuBuilder.php +++ b/src/Bundle/ChillTaskBundle/Menu/MenuBuilder.php @@ -24,24 +24,24 @@ use Chill\TaskBundle\Security\Authorization\TaskVoter; use Symfony\Component\Translation\TranslatorInterface; /** - * + * * * @author Julien Fastré */ -class PersonMenuBuilder implements LocalMenuBuilderInterface +class MenuBuilder implements LocalMenuBuilderInterface { /** * * @var TranslatorInterface */ protected $translator; - + /** * * @var AuthorizationCheckerInterface */ protected $authorizationChecker; - + public function __construct( AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translator) @@ -50,31 +50,58 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface $this->authorizationChecker = $authorizationChecker; } - + public function buildMenu($menuId, MenuItem $menu, array $parameters) { - /* @var $person \Chill\PersonBundle\Entity\Person */ - $person = $parameters['person'] ?? null; - - if ($this->authorizationChecker->isGranted(TaskVoter::SHOW, $person)) { - $menu->addChild( - $this->translator->trans('Tasks'), [ - 'route' => 'chill_task_singletask_list', - 'routeParameters' => $menuId === 'person' ? - [ 'person_id' => $person->getId() ] - : - null, - ]) - ->setExtra('order', 400) - ; - if ($menuId === 'section') { - $menu->setExtra('icons', 'tasks'); - } + switch($menuId) { + case 'person': + $this->buildPersonMenu($menu, $parameters); + break; + case 'accompanyingCourse': + $this->buildAccompanyingCourseMenu($menu, $parameters); + break; + case 'section': + $menu->setExtras('icons', 'tasks'); + break; + default: + throw new \LogicException("this menuid $menuId is not implemented"); } } + public function buildPersonMenu($menu, $parameters){ + + //var $person \Chill\PersonBundle\Entity\Person */ + $person = $parameters['person'] ?? null; + + if ($this->authorizationChecker->isGranted(TaskVoter::SHOW, $person)) { + $menu->addChild( + $this->translator->trans('Tasks'), [ + 'route' => 'chill_task_singletask_by-person_list', + 'routeParameters' => + [ 'id' => $person->getId() ] + ]) + ->setExtra('order', 400); + } + } + + public function buildAccompanyingCourseMenu($menu, $parameters){ + + $course = $parameters['accompanyingCourse']; + + if ($this->authorizationChecker->isGranted(TaskVoter::SHOW, $course)) { + $menu->addChild( + $this->translator->trans('Tasks'), [ + 'route' => 'chill_task_singletask_by-course_list', + 'routeParameters' => + [ 'id' => $course->getId() ] + ]) + ->setExtra('order', 400); + } + } + + public static function getMenuIds(): array { - return ['person']; + return ['person', 'accompanyingCourse']; } } diff --git a/src/Bundle/ChillTaskBundle/Menu/UserMenuBuilder.php b/src/Bundle/ChillTaskBundle/Menu/UserMenuBuilder.php index 0330699ad..419dab9b3 100644 --- a/src/Bundle/ChillTaskBundle/Menu/UserMenuBuilder.php +++ b/src/Bundle/ChillTaskBundle/Menu/UserMenuBuilder.php @@ -88,69 +88,49 @@ class UserMenuBuilder implements LocalMenuBuilderInterface if ($ended > 0) { $this->addItemInMenu( $menu, - $user, '%number% tasks over deadline', - 'My tasks over deadline', - SingleTaskRepository::DATE_STATUS_ENDED, $ended, - -15); + -15, + ['new', 'in_progress'], + ['alert'] + ); } if ($warning > 0) { $this->addItemInMenu( $menu, - $user, '%number% tasks near deadline', - 'My tasks near deadline', - SingleTaskRepository::DATE_STATUS_WARNING, $warning, - -14); + -14, + ['new', 'in_progress'], + ['warning'] + ); } $menu->addChild("My tasks", [ - 'route' => 'chill_task_single_my_tasks' + 'route' => 'chill_task_singletask_my_tasks' ]) ->setExtras([ 'order' => -10, 'icon' => 'tasks' ]); - // $menu->addChild("My calendar list", [ - // 'route' => 'chill_calendar_calendar_list', - // 'routeParameters' => [ - // 'user_id' => $user->getId(), - // ] - // ]) - // ->setExtras([ - // 'order' => -9, - // 'icon' => 'tasks' - // ]); - - /* - $menu->addChild("My aside activities", [ - 'route' => 'chill_crud_aside_activity_index' - ]) - ->setExtras([ - 'order' => -10, - 'icon' => 'tasks' - ]); - */ } - protected function addItemInMenu(MenuItem $menu, User $u, $message, $title, $status, $number, $order) + protected function addItemInMenu(MenuItem $menu, $message, $number, $order, array $states = [], array $status = []) { if ($number > 0) { $menu->addChild( $this->translator->transChoice($message, $number), [ - 'route' => 'chill_task_singletask_list', + 'route' => 'chill_task_singletask_my_tasks', 'routeParameters' => [ - 'user_id' => $u->getId(), - 'status' => [ - $status - ], - 'hide_form' => true, - 'title' => $this->translator->trans($title) + 'f' => [ + 'checkboxes' => [ + 'states' => $states, + 'status' => $status + ] + ] ] ]) ->setExtras([ diff --git a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php new file mode 100644 index 000000000..be8833e75 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php @@ -0,0 +1,335 @@ +centerResolverDispatcher = $centerResolverDispatcher; + $this->em = $em; + $this->security = $security; + $this->authorizationHelper = $authorizationHelper; + } + + public function findByCurrentUsersTasks( + ?string $pattern = null, + ?array $flags = [], + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = [] + ): array { + $qb = $this->buildQueryMyTasks($pattern, $flags); + + return $this->getResult($qb, $start, $limit, $orderBy); + } + + public function countByCurrentUsersTasks( + ?string $pattern = null, + ?array $flags = [] + ): int { + return $this->buildQueryMyTasks($pattern, $flags) + ->select('COUNT(t)') + ->getQuery()->getSingleScalarResult(); + } + + public function findByCourse( + AccompanyingPeriod $course, + ?string $pattern = null, + ?array $flags = [], + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = [] + ): array { + $qb = $this->buildQueryByCourse($course, $pattern, $flags); + $qb = $this->addACL($qb, $course); + + return $this->getResult($qb, $start, $limit, $orderBy); + } + + public function countByCourse( + AccompanyingPeriod $course, + ?string $pattern = null, + ?array $flags = [] + ): int { + $qb = $this->buildQueryByCourse($course, $pattern, $flags); + + return $this + ->addACL($qb, $course) + ->select('COUNT(t)') + ->getQuery()->getSingleScalarResult(); + } + + public function findByPerson( + Person $person, + ?string $pattern = null, + ?array $flags = [], + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = [] + ): array { + $qb = $this->buildQueryByPerson($person, $pattern, $flags); + $qb = $this->addACL($qb, $person); + + return $this->getResult($qb, $start, $limit, $orderBy); + } + + public function countByPerson( + Person $person, + ?string $pattern = null, + ?array $flags = [] + ): int { + $qb = $this->buildQueryByPerson($person, $pattern, $flags); + + return $this + ->addACL($qb, $person) + ->select('COUNT(t)') + ->getQuery()->getSingleScalarResult(); + } + + public function countByAllViewable( + ?string $pattern = null, + ?array $flags = [] + ): int { + $qb = $this->buildBaseQuery($pattern, $flags); + + return $this + ->addACLGlobal($qb) + ->select('COUNT(t)') + ->getQuery()->getSingleScalarResult(); + } + + public function findByAllViewable( + ?string $pattern = null, + ?array $flags = [], + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = [] + ): array { + $qb = $this->buildBaseQuery($pattern, $flags); + $qb = $this->addACLGlobal($qb); + + return $this->getResult($qb, $start, $limit, $orderBy); + } + + public function buildQueryByCourse( + AccompanyingPeriod $course, + ?string $pattern = null, + ?array $flags = [] + ) : QueryBuilder { + $qb = $this->buildBaseQuery($pattern, $flags); + + return $qb + ->andWhere($qb->expr()->eq('t.course', ':course')) + ->setParameter('course', $course) + ; + } + + public function buildQueryByPerson( + Person $person, + ?string $pattern = null, + ?array $flags = [] + ): QueryBuilder + { + $qb = $this->buildBaseQuery($pattern, $flags); + + return $qb + ->andWhere($qb->expr()->eq('t.person', ':person')) + ->setParameter('person', $person); + } + + public function buildQueryMyTasks( + ?string $pattern = null, + ?array $flags = [] + ): QueryBuilder { + $qb = $this->buildBaseQuery($pattern, $flags); + + return $qb + ->andWhere($qb->expr()->eq('t.assignee', ':user')) + ->setParameter('user', $this->security->getUser()) + ; + } + + public function getResult( + QueryBuilder $qb, + ?int $start = 0, + ?int $limit = 50, + ?array $orderBy = [] + ): array { + $qb->select('t'); + + $qb + ->setFirstResult($start) + ->setMaxResults($limit) + ; + + foreach ($orderBy as $field => $direction) { + $qb->addOrderBy('t.'.$field, $direction); + } + + return $qb->getQuery()->getResult(); + } + + private function addACL( + QueryBuilder $qb, + $entity + ): QueryBuilder { + $scopes = $this->authorizationHelper->getReachableScopes($this->security->getUser(), + TaskVoter::SHOW, $this->centerResolverDispatcher->resolveCenter($entity)); + + return $qb->andWhere($qb->expr()->in('t.circle', ':scopes')) + ->setParameter('scopes', $scopes); + } + + private function addACLGlobal( + QueryBuilder $qb + ): QueryBuilder { + $allowedCenters = $this->authorizationHelper + ->getReachableCenters($this->security->getUser(), TaskVoter::SHOW); + + if ([] === $allowedCenters) { + $qb + ->andWhere($qb->expr()->lt('t.id', ':falseid')) + ->setParameter('falseid', -1); + } + + $qb->leftJoin('t.person', 'person') + ->leftJoin('t.course', 'course') + ->leftJoin('course.participations', 'participation') + ->leftJoin('participation.person', 'person_p') + ; + $qb->distinct(true); + + $k = 0; + $orX = $qb->expr()->orX(); + foreach ($allowedCenters as $center) { + $allowedScopes = $this->authorizationHelper->getReachableScopes($this->security->getUser(), + TaskVoter::SHOW, $center); + + $and = $qb->expr()->andX( + $qb->expr()->orX( + $qb->expr()->eq('person.center', ':center_'.$k), + $qb->expr()->eq('person_p.center', ':center_'.$k) + ), + $qb->expr()->in('t.circle', ':scopes_'.$k) + ); + $qb + ->setParameter('center_'.$k, $center) + ->setParameter('scopes_'.$k, $allowedScopes); + $orX->add($and); + + $k++; + } + $qb->andWhere($orX); + + return $qb; + } + + public function buildBaseQuery ( + ?string $pattern = null, + ?array $flags = [] + ): QueryBuilder { + $qb = $this->em->createQueryBuilder(); + $qb + ->from(SingleTask::class, 't') + ; + + if (!empty($pattern)) { + $qb->andWhere($qb->expr()->like('LOWER(UNACCENT(t.title))', 'LOWER(UNACCENT(:pattern))')) + ->setParameter('pattern', '%'.$pattern.'%') + ; + } + + if (count($flags) > 0) { + $orXDate = $qb->expr()->orX(); + $orXState = $qb->expr()->orX(); + $now = new \DateTime(); + + foreach ($flags as $key => $flag) { + switch ($flag) { + case 'no-alert': + $orXDate + ->add( + $qb->expr()->orX( + $qb->expr()->isNull('t.endDate'), + $qb->expr()->gte('t.endDate - COALESCE(t.warningInterval, :intervalBlank)', ':now') + ) + ); + $qb + ->setParameter('intervalBlank', new \DateInterval('P0D')) + ->setParameter('now', $now); + break; + case 'warning': + $orXDate + ->add( + $qb->expr()->andX( + $qb->expr()->not($qb->expr()->isNull('t.endDate')), + $qb->expr()->not($qb->expr()->isNull('t.warningInterval')), + $qb->expr()->lte('t.endDate - t.warningInterval', ':now'), + $qb->expr()->gt('t.endDate', ':now') + ) + ); + $qb + ->setParameter('now', $now); + break; + case 'alert': + $orXDate + ->add( + $qb->expr()->andX( + $qb->expr()->not($qb->expr()->isNull('t.endDate')), + $qb->expr()->lte('t.endDate', ':now') + ) + ); + $qb + ->setParameter('now', $now); + break; + case 'state_new': + $orXState + ->add( + "JSONB_ARRAY_LENGTH(t.currentStates) = 0" + ); + break; + case \substr($flag, 0, 6) === 'state_': + $state = \substr($flag, 6); + $orXState + ->add( + "JSONB_EXISTS_IN_ARRAY(t.currentStates, :state_$key) = 'TRUE'" + ); + $qb->setParameter("state_$key", $state); + break; + default: + throw new \LogicException("this flag is not supported: $flag"); + } + } + + if ($orXDate->count() > 0) { + $qb->andWhere($orXDate); + } + if ($orXState->count() > 0) { + $qb->andWhere($orXState); + } + } + + return $qb; + } + +} diff --git a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php new file mode 100644 index 000000000..58532daa0 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php @@ -0,0 +1,56 @@ +andWhere($qb->expr()->isNull('st.assignee')); } @@ -139,7 +139,7 @@ class SingleTaskRepository extends EntityRepository $qb->andWhere($qb->expr()->eq('st.circle', ':scope')); $qb->setParameter('scope', $params['scope']); } - + if (\array_key_exists('types', $params) && $params['types'] !== NULL) { if (count($params['types']) > 0) { $qb->andWhere($qb->expr()->in('st.type', ':types')); @@ -154,7 +154,7 @@ class SingleTaskRepository extends EntityRepository if (\array_key_exists('is_closed', $params)) { $qb->andWhere($this->buildIsClosed($qb, !$params['is_closed'])); } - + } protected function addTypeFilter(QueryBuilder $qb, $params) @@ -191,7 +191,7 @@ class SingleTaskRepository extends EntityRepository ->add($this->buildNowIsAfterStartDate($qb, true)) ; } - $qb->setParameter('now', new \DateTime('today'), Type::DATE); + $qb->setParameter('now', new \DateTime('today'), Types::DATE_MUTABLE); $qb->andWhere($andWhere); } diff --git a/src/Bundle/ChillTaskBundle/Resources/public/chill/scss/_task-list.scss b/src/Bundle/ChillTaskBundle/Resources/public/chill/scss/_task-list.scss index 3350a4ace..e69cf0325 100644 --- a/src/Bundle/ChillTaskBundle/Resources/public/chill/scss/_task-list.scss +++ b/src/Bundle/ChillTaskBundle/Resources/public/chill/scss/_task-list.scss @@ -1,13 +1,21 @@ +/* + !!!!!!! + This is a legacy version for task list. The new task are now + layed out in page/tile_list/index.js + !!!!!! +*/ + table.chill-task-list { + .chill-task-list__row > div { margin-bottom: 0.50rem; } - + .chill-task-list__row__title { font-weight: bold; font-size: 1.40rem; } - + .chill-task-list__row__type { font-variant: small-caps; display: inline; @@ -21,31 +29,31 @@ table.chill-task-list { border: 1px solid var(--chill-dark-gray); color: var(--chill-dark-gray); } - + .chill-task-list__row__person-for { display: inline; font-weight: bold; } - + .chill-task-list__row__assignee { display: inline; - + } - + .chill_task-list__row__assignee_by { display: inline; font-weight: bold; } - + .chill-task-list__row__dates { & > ul { display: inline; list-style: none; - + & > li { display: inline; margin-right: 0.25rem; - + } } } diff --git a/src/Bundle/ChillTaskBundle/Resources/public/chill/scss/_task-statuses.scss b/src/Bundle/ChillTaskBundle/Resources/public/chill/scss/_task-statuses.scss index 5e371359e..4f598fc7b 100644 --- a/src/Bundle/ChillTaskBundle/Resources/public/chill/scss/_task-statuses.scss +++ b/src/Bundle/ChillTaskBundle/Resources/public/chill/scss/_task-statuses.scss @@ -1,34 +1,22 @@ +// Access to Bootstrap variables and mixins +@import '~ChillMainAssets/module/bootstrap/shared'; + .task-status { - &.box { - font-variant: small-caps; - display: inline; - padding: .2em .6em .3em; - font-size: 0.88rem; - font-weight: bold; - line-height: 1; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: .25em; - color: white; + + // 'new', 'in_progress', 'closed', 'canceled' + &.place-new { + background-color: $chill-yellow; } - - &.type-task_default { - // 'new', 'in_progress', 'closed', 'canceled' - &.place-new { - background-color: var(--chill-yellow); - } - &.place-in_progress { - background-color: var(--chill-green); - } + &.place-in_progress { + background-color: $chill-green; + } - &.place-closed { - background-color: var(--chill-blue); - } + &.place-closed { + background-color: $chill-blue; + } - &.place-canceled { - background-color: var(--chill-beige); - } + &.place-canceled { + background-color: $chill-beige; } } diff --git a/src/Bundle/ChillTaskBundle/Resources/public/page/tile_list/index.js b/src/Bundle/ChillTaskBundle/Resources/public/page/tile_list/index.js new file mode 100644 index 000000000..a8217c7a7 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/public/page/tile_list/index.js @@ -0,0 +1 @@ +require("./task_list.scss"); diff --git a/src/Bundle/ChillTaskBundle/Resources/public/page/tile_list/task_list.scss b/src/Bundle/ChillTaskBundle/Resources/public/page/tile_list/task_list.scss new file mode 100644 index 000000000..4c71462d1 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/public/page/tile_list/task_list.scss @@ -0,0 +1,24 @@ +.chill-task-list { + .task-type { + font-variant: small-caps; + display: inline; + padding: 0.05rem .15rem; + font-size: 0.88rem; + font-weight: light; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border: 1px solid var(--chill-dark-gray); + color: var(--chill-dark-gray); + } + + .assignee { + text-align: right; + } + + .dates { + text-align: right; + } + +} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/confirm_delete.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/confirm_delete.html.twig new file mode 100644 index 000000000..02a0aa31d --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/confirm_delete.html.twig @@ -0,0 +1,19 @@ +{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} + +{% set activeRouteKey = 'chill_task_task_list' %} +{% set course = task.course %} + +{% block title 'Remove task'|trans %} + +{% block content %} + + {{ include('@ChillMain/Util/confirmation_template.html.twig', + { + 'title' : 'Remove task'|trans, + 'confirm_question' : 'Are you sure you want to remove the task "%title%" ?'|trans({ '%title%' : task.title } ), + 'cancel_route' : 'chill_task_singletask_by-course_list', + 'cancel_parameters' : {'id' : task.course.id }, + 'form' : delete_form, + } ) }} + +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/edit.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/edit.html.twig new file mode 100644 index 000000000..ccf0bab04 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/edit.html.twig @@ -0,0 +1,15 @@ +{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} + +{% set activeRouteKey = 'chill_task_single_task_edit' %} +{% set course = task.course %} + +{% block title %} + {{ 'Edit task'|trans }} +{% endblock %} + +{% block content %} + + {% include '@ChillTask/SingleTask/_edit.html.twig' %} + + +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/list.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/list.html.twig new file mode 100644 index 000000000..d00ee7a5e --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/list.html.twig @@ -0,0 +1,46 @@ +{% extends '@ChillPerson/AccompanyingCourse/layout.html.twig' %} + +{% block title 'Tasks for this accompanying period'|trans %} + +{% block content %} +
+ +

{{ block('title') }}

+ + {{ filter_order|chill_render_filter_order_helper }} + + {% if tasks|length == 0 %} +

{{ 'Any tasks'|trans }}

+ {% else %} +
+ {% for task in tasks %} + {% include 'ChillTaskBundle:SingleTask/List:index_item.html.twig' with { 'showContext' : false } %} + {% endfor %} +
+ {% endif %} + + {{ chill_pagination(paginator) }} + + {% if is_granted('CHILL_TASK_TASK_CREATE_FOR_COURSE', accompanyingCourse) %} + + {% endif %} + +
+{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('page_task_list') }} +{% endblock %} +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('page_task_list') }} +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/new.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/new.html.twig new file mode 100644 index 000000000..b8f810540 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/new.html.twig @@ -0,0 +1,13 @@ +{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} + +{% set activeRouteKey = 'chill_task_single_task_new' %} +{# {% set person = task.person %} #} + +{% block title %} + {{ 'New task'|trans }} +{% endblock %} + +{% block content %} + {% include '@ChillTask/SingleTask/_new.html.twig' %} + +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/show.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/show.html.twig new file mode 100644 index 000000000..06ff62a60 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/show.html.twig @@ -0,0 +1,17 @@ +{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} + + +{% set activeRouteKey = 'chill_task_single_task_show' %} +{% set accompanyingCourse = task.course %} + +{% block title %} + {{ 'Task'|trans }} +{% endblock %} + + +{% block content %} + + {% include '@ChillTask/SingleTask/_show.html.twig' %} + + +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/transition.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/transition.html.twig new file mode 100644 index 000000000..08bedd31c --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/AccompanyingCourse/transition.html.twig @@ -0,0 +1,12 @@ +{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} + +{% set activeRouteKey = 'chill_task_task_list' %} +{% set accompanyingCourse = task.course %} + +{% block title 'Remove task'|trans %} + +{% block content %} + + {% include '@ChillTask/SingleTask/_transition.html.twig' %} + +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig new file mode 100644 index 000000000..8fe45959e --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig @@ -0,0 +1,34 @@ +{% extends 'ChillMainBundle::layout.html.twig' %} + +{% block title 'Tasks'|trans %} + +{% block content %} +
+ +

{{ block('title') }}

+ + {{ filter_order|chill_render_filter_order_helper }} + + {% if tasks|length == 0 %} +

{{ 'Any tasks'|trans }}

+ {% else %} +
+ {% for task in tasks %} + {% include 'ChillTaskBundle:SingleTask/List:index_item.html.twig' with { 'showContext' : true} %} + {% endfor %} +
+ {% endif %} + + {{ chill_pagination(paginator) }} + +
+{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('page_task_list') }} +{% endblock %} +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('page_task_list') }} +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index_item.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index_item.html.twig new file mode 100644 index 000000000..284769448 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index_item.html.twig @@ -0,0 +1,125 @@ +
+
+
+
+ {{ task.title }} + {% for place in workflow_marked_places(task) %} + + {{ place|trans }} + + {% endfor %} +
+ {% if task.type != 'task_default'%} + + {{ task_workflow_metadata(task, 'definition.name')|trans }} + + {% endif %} + {% if showContext %} +
+ {% if task.person is not null %} + + {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { + targetEntity: { name: 'person', id: task.person.id }, + action: 'show', + displayBadge: true, + buttonText: task.person|chill_entity_render_string + } %} + + {% elseif task.course is not null %} + + + + + {% for part in task.course.currentParticipations %} + {% 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 + } %} + {% endfor %} + + {% endif %} +
+ {% endif %} +
+ + +
+
+ {% if task.assignee is not null %} +
+ {{ task.assignee|chill_entity_render_box }} +
+ {% endif %} + + {% if task.startDate is not null or task.warningDate is not null or task.endDate is not null %} +
+
    + {% if task.startDate is not null %} +
  • + + {{ task.startDate|format_date('medium') }} +
  • + {% endif %} + {% if task.warningDate is not null %} +
  • + + {{ task.warningDate|format_date('medium') }} +
  • + {% endif %} + {% if task.endDate is not null %} +
  • + + {{ task.endDate|format_date('medium') }} +
  • + {% endif %} +
+
+ {% endif %} +
+ +
+ +
+
+ + +
+
diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index_my_tasks.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index_my_tasks.html.twig new file mode 100644 index 000000000..5d7e88542 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index_my_tasks.html.twig @@ -0,0 +1,34 @@ +{% extends 'ChillMainBundle::layout.html.twig' %} + +{% block title 'My tasks'|trans %} + +{% block content %} +
+ +

{{ block('title') }}

+ + {{ filter_order|chill_render_filter_order_helper }} + + {% if tasks|length == 0 %} +

{{ 'Any tasks'|trans }}

+ {% else %} +
+ {% for task in tasks %} + {% include 'ChillTaskBundle:SingleTask/List:index_item.html.twig' with { 'showContext' : true} %} + {% endfor %} +
+ {% endif %} + + {{ chill_pagination(paginator) }} + +
+{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('page_task_list') }} +{% endblock %} +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('page_task_list') }} +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/confirm_delete.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/confirm_delete.html.twig similarity index 100% rename from src/Bundle/ChillTaskBundle/Resources/views/SingleTask/confirm_delete.html.twig rename to src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/confirm_delete.html.twig diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/edit.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/edit.html.twig new file mode 100644 index 000000000..6188bd859 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/edit.html.twig @@ -0,0 +1,31 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} +{% extends person is defined ? "@ChillPerson/Person/layout.html.twig" %} + +{% set activeRouteKey = 'chill_task_single_task_edit' %} +{% set person = task.person %} + +{% block title %} + {{ 'Edit task'|trans }} +{% endblock %} + +{% block personcontent %} + + {% include '@ChillTask/SingleTask/_edit.html.twig' %} + + +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/list.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/list.html.twig new file mode 100644 index 000000000..ee236b644 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/list.html.twig @@ -0,0 +1,44 @@ +{% extends '@ChillPerson/Person/layout.html.twig' %} + +{% set activeRouteKey = '' %} + +{% block title 'Tasks for {{ name }}'|trans({ '{{ name }}' : person|chill_entity_render_string }) %} + +{% block personcontent %} +
+ +

{{ block('title') }}

+ + {{ filter_order|chill_render_filter_order_helper }} + + {% if tasks|length == 0 %} +

{{ 'Any tasks'|trans }}

+ {% else %} +
+ {% for task in tasks %} + {% include 'ChillTaskBundle:SingleTask/List:index_item.html.twig' with { 'showContext' : false } %} + {% endfor %} +
+ {% endif %} + + {{ chill_pagination(paginator) }} + + {% if is_granted('CHILL_TASK_TASK_CREATE_FOR_PERSON', person) %} + + {% endif %} + +
+{% endblock %} + +{% block css %} + {{ encore_entry_link_tags('page_task_list') }} +{% endblock %} +{% block js %} + {{ encore_entry_script_tags('page_task_list') }} +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/new.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/new.html.twig similarity index 51% rename from src/Bundle/ChillTaskBundle/Resources/views/SingleTask/new.html.twig rename to src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/new.html.twig index 8f6ba2a4b..226956aec 100644 --- a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/new.html.twig +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/new.html.twig @@ -14,37 +14,17 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . #} -{% extends "@ChillPerson/Person/layout.html.twig" %} +{% extends person is defined ? "@ChillPerson/Person/layout.html.twig" : "@ChillPerson/AccompanyingCourse/layout.html.twig" %} {% set activeRouteKey = 'chill_task_single_task_new' %} {% set person = task.person %} -{% block title %}{{ 'New task'|trans }}{% endblock %} +{% block title %} + {{ 'New task'|trans }} +{% endblock %} + {% block personcontent %} -
- -

{{ 'New task'|trans }}

+ {% include '@ChillTask/SingleTask/_new.html.twig' %} - {{ form_start(form) }} - - {{ form_errors(form) }} - - {{ form_row(form.title) }} - {{ form_row(form.description) }} - {{ form_row(form.assignee) }} - {{ form_row(form.circle) }} - {{ form_row(form.startDate) }} - {{ form_row(form.endDate) }} - {{ form_row(form.warningInterval) }} - -
    -
  • - {{ form_widget(form.submit, { 'label': 'Add a new task'|trans, 'attr': {'class': 'btn btn-save'} }) }} -
  • -
- - {{ form_end(form) }} - -
{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/show.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/show.html.twig new file mode 100644 index 000000000..3ac97f0f3 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/show.html.twig @@ -0,0 +1,33 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} +{% extends person is defined ? "@ChillPerson/Person/layout.html.twig" : "@ChillPerson/AccompanyingCourse/layout.html.twig" %} + + +{% set activeRouteKey = 'chill_task_single_task_show' %} +{% set person = task.person %} + +{% block title %} + {{ 'Task'|trans }} +{% endblock %} + + +{% block personcontent %} + + {% include '@ChillTask/SingleTask/_show.html.twig' %} + + +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/transition.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/transition.html.twig new file mode 100644 index 000000000..05009a2f0 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/Person/transition.html.twig @@ -0,0 +1,12 @@ +{% extends "@ChillPerson/Person/layout.html.twig" %} + +{% set activeRouteKey = 'chill_task_task_list' %} +{% set person = task.person %} + +{% block title 'Remove task'|trans %} + +{% block personcontent %} + +{% include '@ChillTask/SingleTask/_transition.html.twig' %} + +{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_edit.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_edit.html.twig new file mode 100644 index 000000000..d0a906c9c --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_edit.html.twig @@ -0,0 +1,27 @@ +
+ +

{{ 'Edit task'|trans }}

+ + {{ form_start(form) }} + + {{ form_row(form.title) }} + {{ form_row(form.description) }} + {{ form_row(form.assignee) }} + {% if form.circle is defined %} + {{ form_row(form.circle) }} + {% endif %} + {{ form_row(form.startDate) }} + {{ form_row(form.endDate) }} + {{ form_row(form.warningInterval) }} + +
    +
  • + + {{ 'Cancel'|trans }} +
  • +
  • + {{ form_widget(form.submit, { 'label': 'Save task', 'attr': {'class' : 'btn btn-update'}})}} +
  • +
+ {{ form_end(form) }} +
diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_list.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_list.html.twig deleted file mode 100644 index a5a867dcc..000000000 --- a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_list.html.twig +++ /dev/null @@ -1,244 +0,0 @@ -{% macro date_status(title, tasks, count, paginator, status, isSingleStatus, person, user) %} - {% if tasks|length > 0 %} -

{{ title|trans }}

- - - - {% for task in tasks %} - - - - - {% endfor %} - -
-
- {{ task.title }} -
- - {% if person is null %} -
- {{ 'For person'|trans }} : {{ task.person}} -
- {% endif %} - -
- {{ task_workflow_metadata(task, 'definition.name')|trans }} -
- -
- {% for place in workflow_marked_places(task) %} - {{ place|trans }} - {% endfor %} - {% if task.assignee is not null %} -
{{ 'By'|trans }} : {{ task.assignee.username }}
- {% endif %} -
- - {% if task.startDate is not null or task.warningDate is not null or task.endDate is not null %} -
-
    - {% if task.startDate is not null %} -
  • - {{ task.startDate|format_date('medium') }} -
  • - {% endif %} - {% if task.warningDate is not null %} -
  • - {{ task.warningDate|format_date('medium') }} -
  • - {% endif %} - {% if task.endDate is not null %} -
  • - {{ task.endDate|format_date('medium') }} -
  • - {% endif %} -
-
- {% endif %} - -
- -
- - {% if isSingleStatus %} - {% if tasks|length < paginator.getTotalItems %} - {{ chill_pagination(paginator) }} - {% endif %} - - - - {% else %} - - {% endif %} - - {% endif %} -{% endmacro %} - -{% import _self as helper %} - -

{{ app.request.query.get('title', null)|escape('html')|default('Task list'|trans) }}

- - {% if false == app.request.query.boolean('hide_form', false) %} -

{{ 'Filter the tasks'|trans }}

- {{ form_start(form) }} - {{ form_row(form.user_id) }} - - {% if form.status is defined %} - {{ form_row(form.status) }} - {% endif %} - - {% if form.types is defined %} - {{ form_row(form.types) }} - {% endif %} - - {% if form.person_id is defined %} - {{ form_row(form.person_id) }} - {% endif %} - - {% if form.center_id is defined %} - {{ form_row(form.center_id) }} - {% endif %} - -
    -
  • - -
  • -
- - {{ form_end(form)}} - {% endif %} - - {% if tasks_count == 0 %} -

{{ "There is no tasks."|trans }}

- {% if person is not null and is_granted('CHILL_TASK_TASK_CREATE', person) %} - - {% endif %} - {% else %} - - {% if false == app.request.query.boolean('hide_form', false) %} -

{{ 'Tasks'|trans }}

- {% endif %} - - {% if person is not null and is_granted('CHILL_TASK_TASK_CREATE', person) %} - - {% endif %} - - {% if single_task_ended_tasks is defined %} - {{ helper.date_status('Tasks with expired deadline', single_task_ended_tasks, single_task_ended_count, single_task_ended_paginator, 'ended', isSingleStatus, person) }} - {% endif %} - - {% if single_task_warning_tasks is defined %} - {{ helper.date_status('Tasks with warning deadline reached', single_task_warning_tasks, single_task_warning_count, single_task_warning_paginator, 'warning', isSingleStatus, person) }} - {% endif %} - - {% if single_task_current_tasks is defined %} - {{ helper.date_status('Current tasks', single_task_current_tasks, single_task_current_count, single_task_current_paginator, 'current', isSingleStatus, person) }} - {% endif %} - - {% if single_task_not_started_tasks is defined %} - {{ helper.date_status('Tasks not started', single_task_not_started_tasks, single_task_not_started_count, single_task_not_started_paginator, 'not_started', isSingleStatus, person) }} - {% endif %} - - {% if single_task_closed_tasks is defined %} - {{ helper.date_status('Closed tasks', single_task_closed_tasks, single_task_closed_count, single_task_closed_paginator, 'closed', isSingleStatus, person) }} - {% endif %} - - {% if isSingleStatus == false %} - - {% endif %} - -{% endif %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_new.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_new.html.twig new file mode 100644 index 000000000..9d324ea85 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_new.html.twig @@ -0,0 +1,32 @@ +
+ +

{{ 'New task'|trans }}

+ + {{ form_start(form) }} + + {{ form_errors(form) }} + + {{ form_row(form.title) }} + {{ form_row(form.description) }} + {{ form_row(form.assignee) }} + {% if form.circle is defined %} + {{ form_row(form.circle) }} + {% endif %} + {{ form_row(form.startDate) }} + {{ form_row(form.endDate) }} + {{ form_row(form.warningInterval) }} + +
    +
  • + + {{'Cancel'|trans}} + +
  • +
  • + {{ form_widget(form.submit, { 'label': 'Add a new task'|trans, 'attr': {'class': 'btn btn-save'} }) }} +
  • +
+ + {{ form_end(form) }} + +
diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_show.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_show.html.twig new file mode 100644 index 000000000..26d269191 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_show.html.twig @@ -0,0 +1,113 @@ +
+ +

{{ 'Task'|trans }}

+ +

{{ task.title }} + {% for place in workflow_marked_places(task) %} + {{ place|trans }} + {% endfor %} +

+ +
+ +
{{ 'Description'|trans }}
+
+ {% if task.description is empty %} + {{"No description"|trans}} + {% else %} +
+ {{ task.description|chill_markdown_to_html }} +
+ {% endif %} +
+ +
{{ 'Assignee'|trans }}
+
+ {% if task.assignee is null %} + {{"No one assignee"|trans}} + {% else %} + {{ task.assignee }} + {% endif %} +
+ + {% if task.scope is not null %} +
{{ 'Scope'|trans }}
+
+ {{ task.scope.name|localize_translatable_string }} +
+ {% endif %} + +

{{"Dates"|trans}}

+ {% if task.startDate is null and task.endDate is null and task.warningDate is null %} +
+
+ {{"No dates specified"|trans}} +
+ + {% else %} + {% if task.startDate is not null %} +
{{ 'Start'|trans }}
+
{{ task.startDate|format_date('long') }}
+ {% endif %} + + {% if task.endDate is not null %} +
{{ 'End'|trans }}
+
{{ task.endDate|format_date('long') }}
+ {% endif %} + + {% if task.warningDate is not null %} +
{{ 'Warning'|trans }}
+
{{ task.warningDate|format_date('long') }}
+ {% endif %} + + {% endif %} +
+ +{% if timeline is not null %} +

{{"Timeline"|trans}}

+ {{ timeline|raw }} +{% endif %} + +
diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_transition.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_transition.html.twig new file mode 100644 index 000000000..8f6345352 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/_transition.html.twig @@ -0,0 +1,23 @@ +

{{ 'Apply transition on task %title%'|trans({ '%title%': task.title } )|raw }}

+ + +{% if task_workflow_metadata(task, 'transition.sentence_confirmation', transition) is not empty %} +

{{ task_workflow_metadata(task, 'transition.sentence_confirmation', transition)|trans }}

+{% else %} +

{{ 'Are you sure to apply the transition %name% on this task ?'|trans({ '%name%': task_workflow_metadata(task, 'transition.name', transition)|default(transition.name)|trans }) }}

+{% endif %} + +{{ form_start(form) }} + +
    +
  • + + {{ 'Back to the list'|trans }} + +
  • +
  • + {{ form_widget(form.submit, { 'attr' : { 'class' : "btn btn-task-exchange green" }, 'label': task_workflow_metadata(task, 'transition.apply_transition_submit_label', transition)|default('apply')|trans } ) }} +
  • +
+ +{{ form_end(form) }} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/edit.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/edit.html.twig deleted file mode 100644 index 67604f015..000000000 --- a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/edit.html.twig +++ /dev/null @@ -1,59 +0,0 @@ -{# - * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . -#} -{% extends "@ChillPerson/Person/layout.html.twig" %} - -{% set activeRouteKey = 'chill_task_single_task_edit' %} -{% set person = task.person %} - -{% block title %}{{ 'Edit task'|trans }}{% endblock %} - -{% block personcontent %} -
- -

{{ 'Edit task'|trans }}

- - {{ form_start(form) }} - - {{ form_row(form.title) }} - {{ form_row(form.description) }} - {{ form_row(form.assignee) }} - {{ form_row(form.circle) }} - {{ form_row(form.startDate) }} - {{ form_row(form.endDate) }} - {{ form_row(form.warningInterval) }} - - - - {{ form_end(form) }} - -
-{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/index.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/index.html.twig index 2a33fc10c..3cd2a1971 100644 --- a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/index.html.twig +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/index.html.twig @@ -15,30 +15,32 @@ * along with this program. If not, see . #} -{% extends '@ChillPerson/Person/layout.html.twig' %} +{% extends layout %} {% set activeRouteKey = 'chill_task_single_task_new' %} -{% block title %}{{ 'Task list'|trans }}{% endblock %} +{% block title %} + {{ 'Task list'|trans }} +{% endblock %} -{% macro thead() %} -{% endmacro %} +{% macro thead() %}{% endmacro %} -{% macro row(task) %} -{% endmacro %} +{% macro row(task) %}{% endmacro %} {% block filtertasks %} -{% if person is not null %} - {% block personcontent %} -
- {% include 'ChillTaskBundle:SingleTask:_list.html.twig' %} -
- {% endblock %} -{% else %} - {% block content %} -
- {% include 'ChillTaskBundle:SingleTask:_list.html.twig' %} -
- {% endblock %} -{% endif %} + {% if person is not null %} + {% block personcontent %} +
+ {% include '@ChillTask/SingleTask/Person/list.html.twig' %} + +
+ {% endblock %} + {% else %} + {% block content %} +
+ {% include '@ChillTask/SingleTask/AccompanyingCourse/list.html.twig' %} + +
+ {% endblock %} + {% endif %} {% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/show.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/show.html.twig deleted file mode 100644 index 8cfa1ea33..000000000 --- a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/show.html.twig +++ /dev/null @@ -1,139 +0,0 @@ -{# - * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . -#} -{% extends "@ChillPerson/Person/layout.html.twig" %} - -{% set activeRouteKey = 'chill_task_single_task_show' %} -{% set person = task.person %} - -{% block title %}{{ 'Task'|trans }}{% endblock %} - - -{% block personcontent %} -
- -

{{ 'Task'|trans }}

- -

{{ task.title }} {% for place in workflow_marked_places(task) %} - {{ place|trans }} - {% endfor %}

- -
- -
{{ 'Description'|trans }}
-
- {% if task.description is empty %} - {{"No description"|trans}} - {% else %} -
- {{ task.description|chill_markdown_to_html }} -
- {% endif %} -
- -
{{ 'Assignee'|trans }}
-
- {% if task.assignee is null %} - {{"No one assignee"|trans}} - {% else %} - {{ task.assignee }} - {% endif %} -
- -
{{ 'Scope'|trans }}
-
{{ task.scope.name|localize_translatable_string }}
- -

{{"Dates"|trans}}

- {% if task.startDate is null and task.endDate is null and task.warningDate is null %} -
-
{{"No dates specified"|trans}}
- - {% else %} - {% if task.startDate is not null %} -
{{ 'Start'|trans }}
-
{{ task.startDate|format_date('long') }}
- {% endif %} - - {% if task.endDate is not null %} -
{{ 'End'|trans }}
-
{{ task.endDate|format_date('long') }}
- {% endif %} - - {% if task.warningDate is not null %} -
{{ 'Warning'|trans }}
-
{{ task.warningDate|format_date('long') }}
- {% endif %} - - {% endif %} -
- - {% if timeline is not null %} -

{{"Timeline"|trans}}

- {{ timeline|raw }} - {% endif %} - - - -
- -{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/transition.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/transition.html.twig deleted file mode 100644 index 617816e4c..000000000 --- a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/transition.html.twig +++ /dev/null @@ -1,34 +0,0 @@ -{% extends "@ChillPerson/Person/layout.html.twig" %} - -{% set activeRouteKey = 'chill_task_task_list' %} -{% set person = task.person %} - -{% block title 'Remove task'|trans %} - -{% block personcontent %} - -

{{ 'Apply transition on task %title%'|trans({ '%title%': task.title } )|raw }}

- - -{% if task_workflow_metadata(task, 'transition.sentence_confirmation', transition) is not empty %} -

{{ task_workflow_metadata(task, 'transition.sentence_confirmation', transition)|trans }}

-{% else %} -

{{ 'Are you sure to apply the transition %name% on this task ?'|trans({ '%name%': task_workflow_metadata(task, 'transition.name', transition)|default(transition.name)|trans }) }}

-{% endif %} - -{{ form_start(form) }} - -
    -
  • - - {{ 'Back to the list'|trans }} - -
  • -
  • - {{ form_widget(form.submit, { 'attr' : { 'class' : "btn btn-task-exchange green" }, 'label': task_workflow_metadata(task, 'transition.apply_transition_submit_label', transition)|default('apply')|trans } ) }} -
  • -
- -{{ form_end(form) }} - -{% endblock %} diff --git a/src/Bundle/ChillTaskBundle/Security/Authorization/TaskVoter.php b/src/Bundle/ChillTaskBundle/Security/Authorization/TaskVoter.php index 2f760a84b..39d9eef32 100644 --- a/src/Bundle/ChillTaskBundle/Security/Authorization/TaskVoter.php +++ b/src/Bundle/ChillTaskBundle/Security/Authorization/TaskVoter.php @@ -32,23 +32,27 @@ use Psr\Log\LoggerInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Chill\MainBundle\Entity\User; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Chill\TaskBundle\Security\Authorization\AuthorizationEvent; final class TaskVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface { - const CREATE = 'CHILL_TASK_TASK_CREATE'; - const UPDATE = 'CHILL_TASK_TASK_UPDATE'; - const SHOW = 'CHILL_TASK_TASK_SHOW'; + const CREATE_COURSE = 'CHILL_TASK_TASK_CREATE_FOR_COURSE'; + const CREATE_PERSON = 'CHILL_TASK_TASK_CREATE_FOR_PERSON'; const DELETE = 'CHILL_TASK_TASK_DELETE'; + const SHOW = 'CHILL_TASK_TASK_SHOW'; + const UPDATE = 'CHILL_TASK_TASK_UPDATE'; const ROLES = [ - self::CREATE, - self::UPDATE, + self::CREATE_COURSE, + self::CREATE_PERSON, + self::DELETE, self::SHOW, - self::DELETE + self::UPDATE, ]; protected AuthorizationHelper $authorizationHelper; @@ -63,7 +67,9 @@ final class TaskVoter extends AbstractChillVoter implements ProvideRoleHierarchy protected VoterHelperInterface $voter; + public function __construct( + VoterHelperFactoryInterface $voterHelperFactory, AccessDecisionManagerInterface $accessDecisionManager, AuthorizationHelper $authorizationHelper, EventDispatcherInterface $eventDispatcher, @@ -80,7 +86,8 @@ final class TaskVoter extends AbstractChillVoter implements ProvideRoleHierarchy $this->voter = $voterFactory ->generate(AbstractTask::class) ->addCheckFor(AbstractTask::class, self::ROLES) - ->addCheckFor(Person::class, [self::SHOW]) + ->addCheckFor(Person::class, [self::SHOW, self::CREATE_PERSON]) + ->addCheckFor(AccompanyingPeriod::class, [self::SHOW, self::CREATE_COURSE]) ->addCheckFor(null, [self::SHOW]) ->build() ; @@ -89,14 +96,6 @@ final class TaskVoter extends AbstractChillVoter implements ProvideRoleHierarchy public function supports($attribute, $subject) { return $this->voter->supports($attribute, $subject); - /* - return ($subject instanceof AbstractTask && in_array($attribute, self::ROLES)) - || - ($subject instanceof Person && \in_array($attribute, [ self::CREATE, self::SHOW ])) - || - (NULL === $subject && $attribute === self::SHOW ) - ; - */ } /** @@ -132,50 +131,24 @@ final class TaskVoter extends AbstractChillVoter implements ProvideRoleHierarchy // do pre-flight check, relying on other decision manager // those pre-flight check concern associated entities if ($subject instanceof AbstractTask) { + // a user can always see his own tasks + if ($subject->getAssignee() === $token->getUser()) { + return true; + } + if (NULL !== $person = $subject->getPerson()) { if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) { return false; } - } elseif (false) { - // here will come the test if the task is associated to an accompanying course + } elseif (NULL !== $period = $subject->getCourse()) { + if (!$this->accessDecisionManager->decide($token, [AccompanyingPeriodVoter::SEE], $period)) { + return false; + } } } // do regular check. return $this->voter->voteOnAttribute($attribute, $subject, $token); - - /* - if ($subject instanceof AbstractTask) { - if ($subject->getPerson() === null) { - throw new \LogicException("You should associate a person with task " - . "in order to check autorizations"); - } - - $person = $subject->getPerson(); - } elseif ($subject instanceof Person) { - $person = $subject; - } else { - // subject is null. We check that at least one center is reachable - $centers = $this->authorizationHelper->getReachableCenters($token->getUser(), new Role($attribute)); - - return count($centers) > 0; - } - - if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) { - return false; - } - $center = $this->centerResolverDispatcher->resolveCenter($subject); - - if (NULL === $center) { - return false; - } - - return $this->authorizationHelper->userHasAccess( - $token->getUser(), - $subject, - $attribute - ); - */ } public function getRoles() diff --git a/src/Bundle/ChillTaskBundle/Timeline/TaskLifeCycleEventTimelineProvider.php b/src/Bundle/ChillTaskBundle/Timeline/TaskLifeCycleEventTimelineProvider.php index fe5a8f78d..20e6a5480 100644 --- a/src/Bundle/ChillTaskBundle/Timeline/TaskLifeCycleEventTimelineProvider.php +++ b/src/Bundle/ChillTaskBundle/Timeline/TaskLifeCycleEventTimelineProvider.php @@ -38,18 +38,18 @@ use Chill\MainBundle\Timeline\TimelineSingleQuery; class TaskLifeCycleEventTimelineProvider implements TimelineProviderInterface { protected EntityManagerInterface $em; - + protected Registry $registry; - + protected AuthorizationHelper $authorizationHelper; protected Security $security; - - + + const TYPE = 'chill_task.transition'; - + public function __construct( - EntityManagerInterface $em, + EntityManagerInterface $em, Registry $registry, AuthorizationHelper $authorizationHelper, Security $security @@ -64,7 +64,7 @@ class TaskLifeCycleEventTimelineProvider implements TimelineProviderInterface { $metadata = $this->em ->getClassMetadata(SingleTaskPlaceEvent::class); - + switch ($context) { case 'person': [ $where, $parameters ] = $this->getWhereClauseForPerson($args['person']); @@ -157,7 +157,7 @@ class TaskLifeCycleEventTimelineProvider implements TimelineProviderInterface // the parameters - $parameters = []; + $parameters = $circleIds = []; // the clause that we will fill $clause = "{person}.{person_id} = ? AND {task}.{circle} IN ({circle_ids})"; @@ -212,27 +212,27 @@ class TaskLifeCycleEventTimelineProvider implements TimelineProviderInterface '{single_task}' => sprintf('%s.%s', $singleTask->getSchemaName(), $singleTask->getTableName()), '{single_task_event}' => sprintf('%s.%s', $taskEvent->getSchemaName(), $taskEvent->getTableName()), '{task_pk}' => $singleTask->getColumnName('id'), - '{event_fk_task}' => $eventFkTask, + '{event_fk_task}' => $eventFkTask, '{person}' => $person->getTableName(), '{task_person_fk}' => $taskFkPerson, - '{person_pk}' => $personPk + '{person_pk}' => $personPk ] ); - } - - public function getEntities(array $ids) + } + + public function getEntities(array $ids) { $events = $this->em ->getRepository(SingleTaskPlaceEvent::class) ->findBy([ 'id' => $ids ]) ; - + return \array_combine( \array_map(function($e) { return $e->getId(); }, $events ), $events ); - } - + } + public function getEntityTemplate($entity, $context, array $args) { $workflow = $this->registry->get($entity->getTask(), @@ -242,22 +242,22 @@ class TaskLifeCycleEventTimelineProvider implements TimelineProviderInterface // `Notice: Undefined property: Chill\TaskBundle\Entity\Task\SingleTaskPlaceEvent::$getData` // * fix syntax error on $entity->getData['workflow'] // * return null if not set - + $transition = $this->getTransitionByName($entity->getTransition(), $workflow); - + return [ 'template' => 'ChillTaskBundle:Timeline:single_task_transition.html.twig', - 'template_data' => [ + 'template_data' => [ 'context' => $context, 'event' => $entity, 'task' => $entity->getTask(), 'transition' => $transition ] ]; - } - + } + /** - * + * * @param string $name * @param Workflow $workflow * @return \Symfony\Component\Workflow\Transition @@ -270,7 +270,7 @@ class TaskLifeCycleEventTimelineProvider implements TimelineProviderInterface } } } - + public function supportsType($type): bool { return $type === self::TYPE; diff --git a/src/Bundle/ChillTaskBundle/chill.webpack.config.js b/src/Bundle/ChillTaskBundle/chill.webpack.config.js index 83c08a6ce..e96855d80 100644 --- a/src/Bundle/ChillTaskBundle/chill.webpack.config.js +++ b/src/Bundle/ChillTaskBundle/chill.webpack.config.js @@ -1,4 +1,6 @@ module.exports = function(encore, entries) { entries.push(__dirname + '/Resources/public/chill/index.js'); + + encore.addEntry('page_task_list', __dirname + '/Resources/public/page/tile_list/index.js'); }; diff --git a/src/Bundle/ChillTaskBundle/config/services/controller.yaml b/src/Bundle/ChillTaskBundle/config/services/controller.yaml index 533c57f47..76799c72d 100644 --- a/src/Bundle/ChillTaskBundle/config/services/controller.yaml +++ b/src/Bundle/ChillTaskBundle/config/services/controller.yaml @@ -1,11 +1,6 @@ services: - Chill\TaskBundle\Controller\: - resource: '../../Controller' - tags: ['controller.service_arguments'] - - Chill\TaskBundle\Controller\SingleTaskController: - arguments: - $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' - $timelineBuilder: '@chill_main.timeline_builder' - $logger: '@chill.main.logger' - tags: ['controller.service_arguments'] + Chill\TaskBundle\Controller\: + resource: "../../Controller" + autowire: true + autoconfigure: ture + tags: ["controller.service_arguments"] diff --git a/src/Bundle/ChillTaskBundle/config/services/form.yaml b/src/Bundle/ChillTaskBundle/config/services/form.yaml index f82e4a562..728b8d7da 100644 --- a/src/Bundle/ChillTaskBundle/config/services/form.yaml +++ b/src/Bundle/ChillTaskBundle/config/services/form.yaml @@ -1,4 +1,9 @@ services: + Chill\TaskBundle\Form\: + resource: '../../Form/' + autowire: true + autoconfigure: true + Chill\TaskBundle\Form\SingleTaskListType: arguments: $em: '@Doctrine\ORM\EntityManagerInterface' diff --git a/src/Bundle/ChillTaskBundle/config/services/menu.yaml b/src/Bundle/ChillTaskBundle/config/services/menu.yaml index b4ff7a421..a9e1948d6 100644 --- a/src/Bundle/ChillTaskBundle/config/services/menu.yaml +++ b/src/Bundle/ChillTaskBundle/config/services/menu.yaml @@ -1,23 +1,22 @@ services: - Chill\TaskBundle\Menu\UserMenuBuilder: - arguments: - $tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' - $counter: '@Chill\TaskBundle\Templating\UI\CountNotificationTask' - $translator: '@Symfony\Component\Translation\TranslatorInterface' - $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface' - tags: - - { name: 'chill.menu_builder' } - - Chill\TaskBundle\Menu\PersonMenuBuilder: - arguments: - $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface' - $translator: '@Symfony\Component\Translation\TranslatorInterface' - tags: - - { name: 'chill.menu_builder' } - - Chill\TaskBundle\Menu\SectionMenuBuilder: - arguments: - $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface' - $translator: '@Symfony\Component\Translation\TranslatorInterface' - tags: - - { name: 'chill.menu_builder' } + Chill\TaskBundle\Menu\UserMenuBuilder: + arguments: + $tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' + $counter: '@Chill\TaskBundle\Templating\UI\CountNotificationTask' + $translator: '@Symfony\Component\Translation\TranslatorInterface' + $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface' + tags: + - { name: "chill.menu_builder" } + + Chill\TaskBundle\Menu\MenuBuilder: + autowire: true + autoconfigure: true + tags: + - { name: "chill.menu_builder" } + + Chill\TaskBundle\Menu\SectionMenuBuilder: + arguments: + $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface' + $translator: '@Symfony\Component\Translation\TranslatorInterface' + tags: + - { name: "chill.menu_builder" } diff --git a/src/Bundle/ChillTaskBundle/config/services/repositories.yaml b/src/Bundle/ChillTaskBundle/config/services/repositories.yaml index 3cc867d96..7bee5abd0 100644 --- a/src/Bundle/ChillTaskBundle/config/services/repositories.yaml +++ b/src/Bundle/ChillTaskBundle/config/services/repositories.yaml @@ -9,3 +9,9 @@ services: arguments: - "@chill.main.security.authorization.helper" Chill\TaskBundle\Repository\SingleTaskRepository: '@chill_task.single_task_repository' + + Chill\TaskBundle\Repository\SingleTaskAclAwareRepository: + autowire: true + autoconfigure: true + + Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface: '@Chill\TaskBundle\Repository\SingleTaskAclAwareRepository' diff --git a/src/Bundle/ChillTaskBundle/migrations/Version20210909153533.php b/src/Bundle/ChillTaskBundle/migrations/Version20210909153533.php new file mode 100644 index 000000000..8a81b4ab9 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/migrations/Version20210909153533.php @@ -0,0 +1,38 @@ +addSql('ALTER TABLE chill_task.recurring_task ADD course_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_task.recurring_task ADD CONSTRAINT FK_9F663B90591CC992 FOREIGN KEY (course_id) REFERENCES chill_person_accompanying_period (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_9F663B90591CC992 ON chill_task.recurring_task (course_id)'); + $this->addSql('ALTER TABLE chill_task.single_task ADD course_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_task.single_task ADD CONSTRAINT FK_194CB3D8591CC992 FOREIGN KEY (course_id) REFERENCES chill_person_accompanying_period (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_194CB3D8591CC992 ON chill_task.single_task (course_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_task.recurring_task DROP CONSTRAINT FK_9F663B90591CC992'); + $this->addSql('ALTER TABLE chill_task.recurring_task DROP course_id'); + $this->addSql('ALTER TABLE chill_task.single_task DROP CONSTRAINT FK_194CB3D8591CC992'); + $this->addSql('ALTER TABLE chill_task.single_task DROP course_id'); + } +} diff --git a/src/Bundle/ChillTaskBundle/migrations/Version20211029213909.php b/src/Bundle/ChillTaskBundle/migrations/Version20211029213909.php new file mode 100644 index 000000000..1ccf52bcb --- /dev/null +++ b/src/Bundle/ChillTaskBundle/migrations/Version20211029213909.php @@ -0,0 +1,26 @@ +addSql('CREATE INDEX by_end_date ON chill_task.single_task (end_date DESC NULLS FIRST)'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX chill_task.by_end_date'); + } +} diff --git a/src/Bundle/ChillTaskBundle/translations/messages.fr.yml b/src/Bundle/ChillTaskBundle/translations/messages.fr.yml index 71bc22612..5633c0219 100644 --- a/src/Bundle/ChillTaskBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillTaskBundle/translations/messages.fr.yml @@ -1,53 +1,55 @@ -Tasks: 'Tâches' -'New task': 'Nouvelle tâche' -'Add a new task': 'Ajouter une nouvelle tâche' +Tasks: "Tâches" +"New task": "Nouvelle tâche" +"Add a new task": "Ajouter une nouvelle tâche" Title: Titre Description: Description -Assignee: 'Personne assignée' +Assignee: "Personne assignée" Scope: Cercle -'Start date': 'Date de début' -'End date': "Date d'échéance" -'Warning date': "Date d'avertissement" -'Warning interval': "Délai d'avertissement avant la date d'échéance" -'Unknown dates': 'Dates non spécifiées' -'N': '' -'Unit': '' +"Start date": "Date de début" +"End date": "Date d'échéance" +"Warning date": "Date d'avertissement" +"Warning interval": "Délai d'avertissement avant la date d'échéance" +"Unknown dates": "Dates non spécifiées" +"N": "" +"Unit": "" Task: Tâche Details: Détails Person: Personne Date: Date Dates: Dates User: Utilisateur -'Task list': 'Liste des tâches' -'Tasks with expired deadline': "Tâches avec une date d'échéance dépassée" -'Tasks with warning deadline reached': "Tâches avec une date d'avertissement atteinte" -'Current tasks': 'Tâches en cours' -'Closed tasks': Tâches terminées -'Tasks not started': 'Tâches non commencées' -'Task start date': 'Date de début' -'Task warning date': "Date d'avertissement" -'Task end date': "Date d'échéance" -'Start': 'Début' -'Warning': 'Avertissement' -'End': 'Échéance' -'Task type': 'Type' -'Task status': 'Statut' -'Edit the task': 'Modifier la tâche' -'Edit task': 'Modifier la tâche' -'Save task': 'Enregistrer la tâche' -'View the task': 'Voir la tâche' -'Update the task': 'Mettre à jour la tâche' -'Remove task': 'Supprimer la tâche' -'Delete': 'Supprimer' -'Change task status': 'Changer le statut' +"Task list": "Liste des tâches" +"Tasks with expired deadline": "Tâches avec une date d'échéance dépassée" +"Tasks with warning deadline reached": "Tâches avec une date d'avertissement atteinte" +"Current tasks": "Tâches en cours" +"Closed tasks": Tâches terminées +"Tasks not started": "Tâches non commencées" +"Task start date": "Date de début" +"Task warning date": "Date d'avertissement" +"Task end date": "Date d'échéance" +"Start": "Début" +"Warning": "Avertissement" +"End": "Échéance" +"Task type": "Type" +"Task status": "Statut" +"Edit the task": "Modifier la tâche" +"Edit task": "Modifier la tâche" +"Save task": "Enregistrer la tâche" +"View the task": "Voir la tâche" +"Update the task": "Mettre à jour la tâche" +"Remove task": "Supprimer la tâche" +"Delete": "Supprimer" +"Change task status": "Changer le statut" 'Are you sure you want to remove the task about "%name%" ?': 'Êtes-vous sûr·e de vouloir supprimer la tâche de "%name%"?' -'See more': 'Voir plus' -'Associated tasks': 'Tâches associées' -'My tasks': 'Mes tâches' -'No description': 'Pas de description' -'No dates specified': 'Dates non spécifiées' -'No one assignee': 'Aucune personne assignée' -'Task types': Types de tâches +'Are you sure you want to remove the task "%title%" ?': 'Êtes-vous sûr·e de vouloir supprimer la tâche "%title%" ?' +"See more": "Voir plus" +"Associated tasks": "Tâches associées" +"My tasks": "Mes tâches" +"Tasks for this accompanying period": "Tâches pour ce parcours d'accompagnement" +"No description": "Pas de description" +"No dates specified": "Dates non spécifiées" +"No one assignee": "Aucune personne assignée" +"Task types": Types de tâches Days: Jour(s) Weeks: Semaine(s) Months: Mois @@ -61,38 +63,43 @@ Default task: Tâche par défaut Not assigned: Aucun utilisateur assigné For person: Pour By: Par +Any tasks: Aucune tâche # transitions - default task definition -'new': 'nouvelle' -'in_progress': 'en cours' -'closed': 'fermée' -'canceled': 'supprimée' +"new": "nouvelle" +"in_progress": "en cours" +"closed": "fermée" +"canceled": "supprimée" start: démarrer close: clotûrer cancel: annuler Start_verb: Démarrer Close_verb: Clotûrer Set this task to cancel state: Marquer cette tâche comme annulée -'%user% has closed the task': '%user% a fermé la tâche' -'%user% has canceled the task': '%user% a annulé la tâche' -'%user% has started the task': '%user% a commencé la tâche' -'%user% has created the task': '%user% a introduit la tâche' +"%user% has closed the task": "%user% a fermé la tâche" +"%user% has canceled the task": "%user% a annulé la tâche" +"%user% has started the task": "%user% a commencé la tâche" +"%user% has created the task": "%user% a introduit la tâche" Are you sure you want to close this task ?: Êtes-vous sûrs de vouloir clotûrer cette tâche ? Are you sure you want to cancel this task ?: Êtes-vous sûrs de vouloir annuler cette tâche ? Are you sure you want to start this task ?: Êtes-vous sûrs de vouloir démarrer cette tâche ? #Flash messages -'The task is created': 'La tâche a été créée' -'There is no tasks.': Aucune tâche. -'The task has been successfully removed.': 'La tâche a bien été supprimée' -'This form contains errors': 'Ce formulaire contient des erreurs' -'The task has been updated': 'La tâche a été mise à jour' -'The transition is successfully applied': 'La transition a bien été effectuée' -'The transition could not be applied': "La transition n'a pas pu être appliquée" +"The task is created": "La tâche a été créée" +"There is no tasks.": Aucune tâche. +"The task has been successfully removed.": "La tâche a bien été supprimée" +"This form contains errors": "Ce formulaire contient des erreurs" +"The task has been updated": "La tâche a été mise à jour" +"The transition is successfully applied": "La transition a bien été effectuée" +"The transition could not be applied": "La transition n'a pas pu être appliquée" #widget -'%number% tasks over deadline': '{0} Aucune tâche dépassée|{1} Une tâche dépassée | ]1,Inf[ %count% tâches dépassées' -'%number% tasks near deadline': '{0} Aucune tâche en rappel|{1} Une tâche en rappel | ]1,Inf[ %count% tâches en rappel' +"%number% tasks over deadline": "{0} Aucune tâche dépassée|{1} Une tâche dépassée | ]1,Inf[ %count% tâches dépassées" +"%number% tasks near deadline": "{0} Aucune tâche en rappel|{1} Une tâche en rappel | ]1,Inf[ %count% tâches en rappel" + +Tasks near deadline: Tâches à échéance proche +Tasks over deadline: Tâches à échéance dépassée +Tasks without alert: Tâches à échéance future ou sans échéance #title My tasks near deadline: Mes tâches à échéance proche @@ -107,4 +114,4 @@ All centers: Tous les centres CHILL_TASK_TASK_CREATE: Ajouter une tâche CHILL_TASK_TASK_DELETE: Supprimer une tâche CHILL_TASK_TASK_SHOW: Voir une tâche -CHILL_TASK_TASK_UPDATE: Modifier une tâche \ No newline at end of file +CHILL_TASK_TASK_UPDATE: Modifier une tâche diff --git a/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php b/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php index bd40c1108..2ce2c2185 100644 --- a/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php +++ b/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php @@ -173,6 +173,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface /** * @var bool * @ORM\Column(name="contact_data_anonymous", type="boolean", options={"default":false}) + * @Groups({"read"}) */ private bool $contactDataAnonymous = false; diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue index 8b8fc6449..1504bdfb9 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue +++ b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue @@ -5,7 +5,6 @@ @@ -73,6 +90,7 @@