mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-07-22 00:27:43 +00:00
Merge remote-tracking branch 'origin/master' into issue561_avoid_search_address_with_undefined_city
This commit is contained in:
commit
aa356185a5
62
CHANGELOG.md
62
CHANGELOG.md
@ -12,11 +12,67 @@ and this project adheres to
|
|||||||
|
|
||||||
<!-- write down unreleased development here -->
|
<!-- write down unreleased development here -->
|
||||||
* [main] avoid address reference search on undefined post code (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/561)
|
* [main] avoid address reference search on undefined post code (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/561)
|
||||||
|
* [Documents] Validate storedObject and allow for null data (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/565)
|
||||||
|
* [Activity form] invert 'incoming' and 'receiving' in Activity form
|
||||||
|
* [Activity form] keep the same order for 'attendee' field in new and edit form
|
||||||
|
* [list with period] use "sameas" test operator to introduce requestor in list
|
||||||
|
|
||||||
|
## Test releases
|
||||||
|
|
||||||
|
### 2021-04-26
|
||||||
|
|
||||||
|
* [Datepickers] datepickers fixed when using keyboard to enter date (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/545)
|
||||||
|
* [social_action] Display 'agents traitants' in parcours resumé and social action list (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/568)
|
||||||
|
* [Person_search] Closed parcours shown within an accordeon that can be opened/closed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/574)
|
||||||
|
|
||||||
|
### 2021-04-24
|
||||||
|
|
||||||
|
* [notification email on course designation] allow raw string in email content generation
|
||||||
|
* [Accompanying period work] list evaluations associated to a work by startDate, and then by id, from the most recent to older
|
||||||
|
* [Documents] Change wording 'créer' to 'enregistrer' (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/634)
|
||||||
|
* [Parcours]: The number of 'mes parcours' displayed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/572)
|
||||||
|
* [Hompage_widget]: Renaming of tabs and removal of social actions tab (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/570)
|
||||||
|
* [activity]: Ignore thirdparties when creating a social action via an activity (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/573)
|
||||||
|
* [parcours]: change wording of warning message and button when user is not associated to a household yet (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/590#note_918370943)
|
||||||
|
* [Accompanying period work evaluations] list documents associated to a work by creation date, and then by id, from the most recent to older
|
||||||
|
* [Course comment] add validationConstraint NotNull and NotBlank on comment content, to avoid sql error
|
||||||
|
* [Notifications] delay the sending of notificaiton to kernel.terminate
|
||||||
|
* [Notifications / Period user change] fix the sending of notification when user changes
|
||||||
|
* [Activity form] invert 'incoming' and 'receiving' in Activity form
|
||||||
|
* [Activity form] keep the same order for 'attendee' field in new and edit form
|
||||||
|
* [list with period] use "sameas" test operator to introduce requestor in list
|
||||||
|
* [notification email on course designation] allow raw string in email content generation
|
||||||
|
* [Accompanying period work] list evaluations associated to a work by startDate, and then by id, from the most recent to older
|
||||||
|
|
||||||
|
|
||||||
|
### 2021-04-13
|
||||||
|
|
||||||
|
* [person] household address: add a form for editing the validFrom date (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/541)
|
||||||
|
* [person] householdmemberseditor: fix composition type bug in select form (vuejs) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/543)
|
||||||
|
* [docgen] add more persons choices in docgen for course: amongst requestor (if person), resources of course (if person), and PersonResource (if person);
|
||||||
|
* [docgen] add a new context with a list of activities in course
|
||||||
|
* [docgen] add a comment in budget lines
|
||||||
|
* [notifications] allow to send a notification to an email address. The address receive an access link
|
||||||
|
* [adresses] add constraints in database to avoid errors later: postcode not null, and validfrom <= validto
|
||||||
|
* [accompanying work editor] add a label on document title input
|
||||||
|
|
||||||
|
### 2021-04-07
|
||||||
|
|
||||||
|
* notification list: move action buttons outside of the toggle
|
||||||
|
* fix detecting of non-read notification
|
||||||
|
* filter users which are disabled in search user api
|
||||||
|
* order query for location and add pagination in list
|
||||||
|
* allow every person which has part for a workflow to see the workflow page
|
||||||
|
* able to see the workflow if the evaluation document has been deleted
|
||||||
|
* hardcode the list of supported mime types for edition with collabora
|
||||||
|
* list of accompanying course: allow to see the pinned comment in list_item
|
||||||
|
|
||||||
|
### 2021-04-06
|
||||||
|
|
||||||
* [main] notification toggle read: correct js syntax for compilation in production (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/548)
|
* [main] notification toggle read: correct js syntax for compilation in production (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/548)
|
||||||
* [parcours] Display of interlocuteurs changed to flex-table in parcours edit page to prevent cut-off of information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/535)
|
* [parcours] Display of interlocuteurs changed to flex-table in parcours edit page to prevent cut-off of information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/535)
|
||||||
* [activity] espace entre les boutons pour supprimer les documents
|
* [activity] espace entre les boutons pour supprimer les documents
|
||||||
|
|
||||||
## Test releases
|
|
||||||
|
|
||||||
### continuous release in February and March
|
### continuous release in February and March
|
||||||
|
|
||||||
@ -61,6 +117,7 @@ and this project adheres to
|
|||||||
* [household] bugfix if position of member is null, renderbox no longer throws an error (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/480)
|
* [household] bugfix if position of member is null, renderbox no longer throws an error (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/480)
|
||||||
* [parcours] location cannot be removed if linked to a user (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/478)
|
* [parcours] location cannot be removed if linked to a user (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/478)
|
||||||
* [person] email added to twig personRenderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/490)
|
* [person] email added to twig personRenderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/490)
|
||||||
|
* [activity] Only youngest descendant is kept for social issues and actions (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/471)
|
||||||
* [person] Add link to current household in person banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/484)
|
* [person] Add link to current household in person banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/484)
|
||||||
* [address] person badge in address history changed to open OnTheFly with all person info (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/489)
|
* [address] person badge in address history changed to open OnTheFly with all person info (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/489)
|
||||||
* [person] Change 'personne' with 'usager' and '&' with 'ET' (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/499)
|
* [person] Change 'personne' with 'usager' and '&' with 'ET' (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/499)
|
||||||
@ -88,6 +145,9 @@ and this project adheres to
|
|||||||
* [notification] Display of social action within workflow notification set to display block (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/537)
|
* [notification] Display of social action within workflow notification set to display block (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/537)
|
||||||
* [onthefly] trim trailing whitespace in email of person and thirdparty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/542)
|
* [onthefly] trim trailing whitespace in email of person and thirdparty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/542)
|
||||||
|
|
||||||
|
* [action] Only youngest descendant is kept for social issues and actions (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/471)
|
||||||
|
## Test releases
|
||||||
|
|
||||||
### test release 2022-02-21
|
### test release 2022-02-21
|
||||||
|
|
||||||
* [notifications] Word 'un' changed to number '1' for notifications in user menu (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/483)
|
* [notifications] Word 'un' changed to number '1' for notifications in user menu (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/483)
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
"knplabs/knp-time-bundle": "^1.12",
|
"knplabs/knp-time-bundle": "^1.12",
|
||||||
"league/csv": "^9.7.1",
|
"league/csv": "^9.7.1",
|
||||||
"nyholm/psr7": "^1.4",
|
"nyholm/psr7": "^1.4",
|
||||||
"ocramius/package-versions": "^1.10",
|
"ocramius/package-versions": "^1.10 || ^2",
|
||||||
"odolbeau/phone-number-bundle": "^3.6",
|
"odolbeau/phone-number-bundle": "^3.6",
|
||||||
"phpoffice/phpspreadsheet": "^1.16",
|
"phpoffice/phpspreadsheet": "^1.16",
|
||||||
"ramsey/uuid-doctrine": "^1.7",
|
"ramsey/uuid-doctrine": "^1.7",
|
||||||
@ -33,6 +33,7 @@
|
|||||||
"symfony/expression-language": "^4.4",
|
"symfony/expression-language": "^4.4",
|
||||||
"symfony/form": "^4.4",
|
"symfony/form": "^4.4",
|
||||||
"symfony/framework-bundle": "^4.4",
|
"symfony/framework-bundle": "^4.4",
|
||||||
|
"symfony/http-foundation": "^4.4",
|
||||||
"symfony/intl": "^4.4",
|
"symfony/intl": "^4.4",
|
||||||
"symfony/mailer": "^5.4",
|
"symfony/mailer": "^5.4",
|
||||||
"symfony/mime": "^5.4",
|
"symfony/mime": "^5.4",
|
||||||
@ -103,7 +104,8 @@
|
|||||||
"ergebnis/composer-normalize": true,
|
"ergebnis/composer-normalize": true,
|
||||||
"ocramius/package-versions": true,
|
"ocramius/package-versions": true,
|
||||||
"phpro/grumphp": true,
|
"phpro/grumphp": true,
|
||||||
"phpstan/extension-installer": true
|
"phpstan/extension-installer": true,
|
||||||
|
"roave/you-are-using-it-wrong": true
|
||||||
},
|
},
|
||||||
"bin-dir": "bin",
|
"bin-dir": "bin",
|
||||||
"optimize-autoloader": true,
|
"optimize-autoloader": true,
|
||||||
|
@ -35,6 +35,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
|||||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||||
use Symfony\Component\Serializer\Annotation\Groups;
|
use Symfony\Component\Serializer\Annotation\Groups;
|
||||||
use Symfony\Component\Serializer\Annotation\SerializedName;
|
use Symfony\Component\Serializer\Annotation\SerializedName;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Activity.
|
* Class Activity.
|
||||||
@ -93,6 +94,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToMany(targetEntity="Chill\DocStoreBundle\Entity\StoredObject", cascade={"persist"})
|
* @ORM\ManyToMany(targetEntity="Chill\DocStoreBundle\Entity\StoredObject", cascade={"persist"})
|
||||||
|
* @Assert\Valid(traverse=true)
|
||||||
*/
|
*/
|
||||||
private Collection $documents;
|
private Collection $documents;
|
||||||
|
|
||||||
@ -231,11 +233,22 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
|||||||
{
|
{
|
||||||
if (!$this->socialActions->contains($socialAction)) {
|
if (!$this->socialActions->contains($socialAction)) {
|
||||||
$this->socialActions[] = $socialAction;
|
$this->socialActions[] = $socialAction;
|
||||||
|
$this->ensureSocialActionConsistency();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a social issue.
|
||||||
|
*
|
||||||
|
* Note: the social issue consistency (the fact that only yougest social issues
|
||||||
|
* are kept) is processed by an entity listener:
|
||||||
|
*
|
||||||
|
* @see{\Chill\PersonBundle\AccompanyingPeriod\SocialIssueConsistency\AccompanyingPeriodSocialIssueConsistencyEntityListener}
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
public function addSocialIssue(SocialIssue $socialIssue): self
|
public function addSocialIssue(SocialIssue $socialIssue): self
|
||||||
{
|
{
|
||||||
if (!$this->socialIssues->contains($socialIssue)) {
|
if (!$this->socialIssues->contains($socialIssue)) {
|
||||||
@ -631,4 +644,13 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ensureSocialActionConsistency(): void
|
||||||
|
{
|
||||||
|
$ancestors = SocialAction::findAncestorSocialActions($this->getSocialActions());
|
||||||
|
|
||||||
|
foreach ($ancestors as $ancestor) {
|
||||||
|
$this->removeSocialAction($ancestor);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,9 +66,6 @@ class ActivityEntityListener
|
|||||||
$newAction->addPerson($person);
|
$newAction->addPerson($person);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($associatedThirdparties as $thirdparty) {
|
|
||||||
$newAction->setHandlingThierparty($thirdparty);
|
|
||||||
}
|
|
||||||
$this->em->persist($newAction);
|
$this->em->persist($newAction);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
}
|
}
|
||||||
|
@ -373,8 +373,8 @@ class ActivityType extends AbstractType
|
|||||||
'label' => $activityType->getLabel('sentReceived'),
|
'label' => $activityType->getLabel('sentReceived'),
|
||||||
'required' => $activityType->isRequired('sentReceived'),
|
'required' => $activityType->isRequired('sentReceived'),
|
||||||
'choices' => [
|
'choices' => [
|
||||||
'Sent' => Activity::SENTRECEIVED_SENT,
|
|
||||||
'Received' => Activity::SENTRECEIVED_RECEIVED,
|
'Received' => Activity::SENTRECEIVED_RECEIVED,
|
||||||
|
'Sent' => Activity::SENTRECEIVED_SENT,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -12,13 +12,20 @@ declare(strict_types=1);
|
|||||||
namespace Chill\ActivityBundle\Repository;
|
namespace Chill\ActivityBundle\Repository;
|
||||||
|
|
||||||
use Chill\ActivityBundle\Entity\Activity;
|
use Chill\ActivityBundle\Entity\Activity;
|
||||||
|
use Chill\ActivityBundle\Entity\ActivityPresence;
|
||||||
|
use Chill\ActivityBundle\Entity\ActivityType;
|
||||||
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
||||||
|
use Chill\MainBundle\Entity\Location;
|
||||||
|
use Chill\MainBundle\Entity\LocationType;
|
||||||
use Chill\MainBundle\Entity\Scope;
|
use Chill\MainBundle\Entity\Scope;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
|
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\AbstractQuery;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
use Symfony\Component\Security\Core\Role\Role;
|
use Symfony\Component\Security\Core\Role\Role;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
@ -72,6 +79,86 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte
|
|||||||
->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy);
|
->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array
|
||||||
|
{
|
||||||
|
$rsm = new ResultSetMappingBuilder($this->em);
|
||||||
|
|
||||||
|
$sql = '
|
||||||
|
SELECT
|
||||||
|
a.id AS activity_id,
|
||||||
|
date,
|
||||||
|
CASE WHEN durationtime IS NOT NULL THEN (EXTRACT(EPOCH from durationtime) / 60)::int ELSE 0 END AS durationtimeminute,
|
||||||
|
attendee_id,
|
||||||
|
comment_comment,
|
||||||
|
emergency,
|
||||||
|
sentreceived,
|
||||||
|
CASE WHEN traveltime IS NOT NULL THEN (EXTRACT(EPOCH from traveltime) / 60)::int ELSE 0 END AS traveltimeminute,
|
||||||
|
t.id AS type_id, t.name as type_name,
|
||||||
|
p.id AS presence_id, p.name AS presence_name,
|
||||||
|
location.id AS location_id, location.address_id, location.name AS location_name, location.phonenumber1, location.phonenumber2, location.email,
|
||||||
|
location.locationtype_id, locationtype.title AS locationtype_title,
|
||||||
|
users.userids AS userids,
|
||||||
|
thirdparties.thirdpartyids,
|
||||||
|
persons.personids,
|
||||||
|
actions.socialactionids,
|
||||||
|
issues.socialissueids
|
||||||
|
|
||||||
|
FROM activity a
|
||||||
|
LEFT JOIN chill_main_location location ON a.location_id = location.id
|
||||||
|
LEFT JOIN chill_main_location_type locationtype ON location.locationtype_id = locationtype.id
|
||||||
|
LEFT JOIN activitytpresence p ON a.attendee_id = p.id
|
||||||
|
LEFT JOIN activitytype t ON a.type_id = t.id
|
||||||
|
LEFT JOIN LATERAL (SELECT jsonb_agg(user_id) userids, activity_id FROM activity_user AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS users ON TRUE
|
||||||
|
LEFT JOIN LATERAL (SELECT jsonb_agg(thirdparty_id) thirdpartyids, activity_id FROM activity_thirdparty AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS thirdparties ON TRUE
|
||||||
|
LEFT JOIN LATERAL (SELECT jsonb_agg(person_id) personids, activity_id FROM activity_person AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS persons ON TRUE
|
||||||
|
LEFT JOIN LATERAL (SELECT jsonb_agg(socialaction_id) socialactionids, activity_id FROM chill_activity_activity_chill_person_socialaction AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS actions ON TRUE
|
||||||
|
LEFT JOIN LATERAL (SELECT jsonb_agg(socialissue_id) socialissueids, activity_id FROM chill_activity_activity_chill_person_socialissue AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS issues ON TRUE
|
||||||
|
|
||||||
|
WHERE accompanyingperiod_id = ?
|
||||||
|
ORDER BY a.date DESC, a.id DESC
|
||||||
|
LIMIT ?
|
||||||
|
';
|
||||||
|
|
||||||
|
$rsm
|
||||||
|
->addEntityResult(Activity::class, 'a')
|
||||||
|
->addFieldResult('a', 'activity_id', 'id')
|
||||||
|
->addFieldResult('a', 'date', 'date')
|
||||||
|
->addFieldResult('a', 'comment', 'comment')
|
||||||
|
->addFieldResult('a', 'sentreceived', 'sentReceived')
|
||||||
|
->addFieldResult('a', 'emergency', 'emergency')
|
||||||
|
->addJoinedEntityResult(Location::class, 'location', 'a', 'location')
|
||||||
|
->addFieldResult('location', 'location_id', 'id')
|
||||||
|
->addFieldResult('location', 'location_name', 'name')
|
||||||
|
->addFieldResult('location', 'phonenumber1', 'phonenumber1')
|
||||||
|
->addFieldResult('location', 'phonenumber2', 'phonenumber2')
|
||||||
|
->addFieldResult('location', 'email', 'email')
|
||||||
|
->addJoinedEntityResult(LocationType::class, 'locationType', 'location', 'locationType')
|
||||||
|
->addFieldResult('locationType', 'locationtype_id', 'id')
|
||||||
|
->addFieldResult('locationType', 'locationtype_title', 'title')
|
||||||
|
->addJoinedEntityResult(ActivityType::class, 'activityType', 'a', 'activityType')
|
||||||
|
->addFieldResult('activityType', 'type_id', 'id')
|
||||||
|
->addFieldResult('activityType', 'type_name', 'name')
|
||||||
|
->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee')
|
||||||
|
->addFieldResult('activityPresence', 'presence_id', 'id')
|
||||||
|
->addFieldResult('activityPresence', 'presence_name', 'name')
|
||||||
|
|
||||||
|
// results which cannot be mapped into entity
|
||||||
|
->addScalarResult('comment_comment', 'comment', Types::TEXT)
|
||||||
|
->addScalarResult('userids', 'userIds', Types::JSON)
|
||||||
|
->addScalarResult('thirdpartyids', 'thirdPartyIds', Types::JSON)
|
||||||
|
->addScalarResult('personids', 'personIds', Types::JSON)
|
||||||
|
->addScalarResult('socialactionids', 'socialActionIds', Types::JSON)
|
||||||
|
->addScalarResult('socialissueids', 'socialIssueIds', Types::JSON)
|
||||||
|
->addScalarResult('durationtimeminute', 'durationTimeMinute', Types::INTEGER)
|
||||||
|
->addScalarResult('traveltimeminute', 'travelTimeMinute', Types::INTEGER);
|
||||||
|
|
||||||
|
$nq = $this->em->createNativeQuery($sql, $rsm);
|
||||||
|
|
||||||
|
$nq->setParameter(0, $period->getId())->setParameter(1, $limit);
|
||||||
|
|
||||||
|
return $nq->getResult(AbstractQuery::HYDRATE_ARRAY);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $orderBy
|
* @param array $orderBy
|
||||||
*
|
*
|
||||||
|
@ -21,6 +21,15 @@ interface ActivityACLAwareRepositoryInterface
|
|||||||
*/
|
*/
|
||||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
|
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of activities, simplified as array (not object).
|
||||||
|
*
|
||||||
|
* The aim of this method is to get a long list of activities and keep performance.
|
||||||
|
*
|
||||||
|
* @return array an array of array, each item representing an activity
|
||||||
|
*/
|
||||||
|
public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Activity[]|array
|
* @return Activity[]|array
|
||||||
*/
|
*/
|
||||||
|
@ -54,19 +54,19 @@
|
|||||||
{{ $t('activity.select_first_a_social_issue') }}
|
{{ $t('activity.select_first_a_social_issue') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<template v-else-if="socialActionsList.length > 0">
|
<template v-else-if="socialActionsList.length > 0">
|
||||||
<check-social-action
|
<check-social-action
|
||||||
v-if="socialIssuesSelected.length || socialActionsSelected.length"
|
v-if="socialIssuesSelected.length || socialActionsSelected.length"
|
||||||
v-for="action in socialActionsList"
|
v-for="action in socialActionsList"
|
||||||
:key="action.id"
|
:key="action.id"
|
||||||
:action="action"
|
:action="action"
|
||||||
:selection="socialActionsSelected"
|
:selection="socialActionsSelected"
|
||||||
@updateSelected="updateActionsSelected">
|
@updateSelected="updateActionsSelected">
|
||||||
</check-social-action>
|
</check-social-action>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<span v-else-if="actionAreLoaded && socialActionsList.length === 0" class="inline-choice chill-no-data-statement mt-3">
|
<span v-else-if="actionAreLoaded && socialActionsList.length === 0" class="inline-choice chill-no-data-statement mt-3">
|
||||||
{{ $t('activity.social_action_list_empty') }}
|
{{ $t('activity.social_action_list_empty') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ export default {
|
|||||||
return this.$store.state.socialIssuesOther;
|
return this.$store.state.socialIssuesOther;
|
||||||
},
|
},
|
||||||
socialActionsList() {
|
socialActionsList() {
|
||||||
return this.$store.state.socialActionsList;
|
return this.$store.getters.socialActionsListSorted;
|
||||||
},
|
},
|
||||||
socialActionsSelected() {
|
socialActionsSelected() {
|
||||||
return this.$store.state.activity.socialActions;
|
return this.$store.state.activity.socialActions;
|
||||||
|
@ -101,6 +101,9 @@ const store = createStore({
|
|||||||
state.activity.activityType.thirdPartiesVisible !== 0)
|
state.activity.activityType.thirdPartiesVisible !== 0)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
socialActionsListSorted(state) {
|
||||||
|
return [ ...state.socialActionsList].sort((a, b) => a.ordering - b.ordering);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
// SocialIssueAcc
|
// SocialIssueAcc
|
||||||
@ -131,7 +134,6 @@ const store = createStore({
|
|||||||
state.socialActionsList = [];
|
state.socialActionsList = [];
|
||||||
},
|
},
|
||||||
addActionInList(state, action) {
|
addActionInList(state, action) {
|
||||||
//console.log('add action list', action.id);
|
|
||||||
state.socialActionsList.push(action);
|
state.socialActionsList.push(action);
|
||||||
},
|
},
|
||||||
updateActionsSelected(state, actions) {
|
updateActionsSelected(state, actions) {
|
||||||
|
@ -85,15 +85,15 @@
|
|||||||
{{ form_row(form.comment) }}
|
{{ form_row(form.comment) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{%- if form.attendee is defined -%}
|
||||||
|
{{ form_row(form.attendee) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{%- if form.documents is defined -%}
|
{%- if form.documents is defined -%}
|
||||||
{{ form_row(form.documents) }}
|
{{ form_row(form.documents) }}
|
||||||
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\ActivityBundle\Entity\Activity" data-entity-id="{{ entity.id }}"></div>
|
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\ActivityBundle\Entity\Activity" data-entity-id="{{ entity.id }}"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{%- if form.attendee is defined -%}
|
|
||||||
{{ form_row(form.attendee) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# TODO .. status #}
|
{# TODO .. status #}
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
@ -0,0 +1,286 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\ActivityBundle\Service\DocGenerator;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Entity\ActivityPresence;
|
||||||
|
use Chill\ActivityBundle\Entity\ActivityType;
|
||||||
|
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\MainBundle\Entity\Location;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Repository\UserRepository;
|
||||||
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||||
|
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
||||||
|
use Chill\PersonBundle\Repository\PersonRepository;
|
||||||
|
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
|
||||||
|
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||||
|
use Chill\PersonBundle\Service\DocGenerator\AccompanyingPeriodContext;
|
||||||
|
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
|
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
|
||||||
|
use DateTime;
|
||||||
|
use libphonenumber\PhoneNumber;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
class ListActivitiesByAccompanyingPeriodContext implements
|
||||||
|
DocGeneratorContextWithAdminFormInterface,
|
||||||
|
DocGeneratorContextWithPublicFormInterface
|
||||||
|
{
|
||||||
|
private AccompanyingPeriodContext $accompanyingPeriodContext;
|
||||||
|
|
||||||
|
private ActivityACLAwareRepositoryInterface $activityACLAwareRepository;
|
||||||
|
|
||||||
|
private NormalizerInterface $normalizer;
|
||||||
|
|
||||||
|
private PersonRepository $personRepository;
|
||||||
|
|
||||||
|
private SocialActionRepository $socialActionRepository;
|
||||||
|
|
||||||
|
private SocialIssueRepository $socialIssueRepository;
|
||||||
|
|
||||||
|
private ThirdPartyRepository $thirdPartyRepository;
|
||||||
|
|
||||||
|
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||||
|
|
||||||
|
private UserRepository $userRepository;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
AccompanyingPeriodContext $accompanyingPeriodContext,
|
||||||
|
ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
|
||||||
|
NormalizerInterface $normalizer,
|
||||||
|
PersonRepository $personRepository,
|
||||||
|
SocialActionRepository $socialActionRepository,
|
||||||
|
SocialIssueRepository $socialIssueRepository,
|
||||||
|
ThirdPartyRepository $thirdPartyRepository,
|
||||||
|
TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
|
UserRepository $userRepository
|
||||||
|
) {
|
||||||
|
$this->accompanyingPeriodContext = $accompanyingPeriodContext;
|
||||||
|
$this->activityACLAwareRepository = $activityACLAwareRepository;
|
||||||
|
$this->normalizer = $normalizer;
|
||||||
|
$this->personRepository = $personRepository;
|
||||||
|
$this->socialActionRepository = $socialActionRepository;
|
||||||
|
$this->socialIssueRepository = $socialIssueRepository;
|
||||||
|
$this->thirdPartyRepository = $thirdPartyRepository;
|
||||||
|
$this->translatableStringHelper = $translatableStringHelper;
|
||||||
|
$this->userRepository = $userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function adminFormReverseTransform(array $data): array
|
||||||
|
{
|
||||||
|
return $this->accompanyingPeriodContext->adminFormReverseTransform($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function adminFormTransform(array $data): array
|
||||||
|
{
|
||||||
|
return $this->accompanyingPeriodContext->adminFormTransform($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildAdminForm(FormBuilderInterface $builder): void
|
||||||
|
{
|
||||||
|
$this->accompanyingPeriodContext->buildAdminForm($builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
|
||||||
|
{
|
||||||
|
$this->accompanyingPeriodContext->buildPublicForm($builder, $template, $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
|
||||||
|
{
|
||||||
|
$data = $this->accompanyingPeriodContext->getData($template, $entity, $contextGenerationData);
|
||||||
|
|
||||||
|
$data['activities'] = $this->getActivitiesSimplified($entity);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'docgen.Accompanying period with a list of activities description';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityClass(): string
|
||||||
|
{
|
||||||
|
return AccompanyingPeriod::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormData(DocGeneratorTemplate $template, $entity): array
|
||||||
|
{
|
||||||
|
return $this->accompanyingPeriodContext->getFormData($template, $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getKey(): string
|
||||||
|
{
|
||||||
|
return self::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'docgen.Accompanying period with a list of activities';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasAdminForm(): bool
|
||||||
|
{
|
||||||
|
return $this->accompanyingPeriodContext->hasAdminForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool
|
||||||
|
{
|
||||||
|
return $this->accompanyingPeriodContext->hasPublicForm($template, $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
||||||
|
{
|
||||||
|
$this->accompanyingPeriodContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getActivitiesSimplified(AccompanyingPeriod $period)
|
||||||
|
{
|
||||||
|
$activities =
|
||||||
|
$this->activityACLAwareRepository->findByAccompanyingPeriodSimplified($period);
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($activities as $row) {
|
||||||
|
$activity = $row[0];
|
||||||
|
|
||||||
|
$activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [
|
||||||
|
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => DateTime::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (null === $activity['location']) {
|
||||||
|
$activity['location'] = $this->normalizer->normalize(null, 'docgen', [
|
||||||
|
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => Location::class,
|
||||||
|
]);
|
||||||
|
$activity['location']['type'] = 'location';
|
||||||
|
} else {
|
||||||
|
$activity['location']['isNull'] = false;
|
||||||
|
$activity['location']['type'] = 'location';
|
||||||
|
|
||||||
|
foreach (['1', '2'] as $key) {
|
||||||
|
$activity['location']['phonenumber' . $key] = $this->normalizer->normalize(
|
||||||
|
$activity['location']['phonenumber' . $key],
|
||||||
|
'docgen',
|
||||||
|
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => PhoneNumber::class]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($activity['location']['locationType']['id'])) {
|
||||||
|
$activity['location']['locationType']['title'] = $this->translatableStringHelper->localize(
|
||||||
|
$activity['location']['locationType']['title']
|
||||||
|
);
|
||||||
|
$activity['location']['locationType']['isNull'] = false;
|
||||||
|
$activity['location']['locationType']['type'] = 'locationType';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $activity['activityType']) {
|
||||||
|
$activity['activityType']['name'] = $this->translatableStringHelper->localize(
|
||||||
|
$activity['activityType']['name']
|
||||||
|
);
|
||||||
|
$activity['activityType']['isNull'] = false;
|
||||||
|
$activity['activityType']['type'] = 'activityType';
|
||||||
|
} else {
|
||||||
|
$activity['activityType'] = $this->normalizer->normalize(null, 'docgen', [
|
||||||
|
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => ActivityType::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $activity['attendee']) {
|
||||||
|
$activity['attendee']['name'] = $this->translatableStringHelper->localize(
|
||||||
|
$activity['attendee']['name']
|
||||||
|
);
|
||||||
|
$activity['attendee']['isNull'] = false;
|
||||||
|
$activity['attendee']['type'] = 'activityPresence';
|
||||||
|
} else {
|
||||||
|
$activity['attendee'] = $this->normalizer->normalize(null, 'docgen', [
|
||||||
|
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => ActivityPresence::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$activity['comment'] = (string) $row['comment'];
|
||||||
|
$activity['travelTimeMinute'] = $row['travelTimeMinute'];
|
||||||
|
$activity['durationTimeMinute'] = $row['durationTimeMinute'];
|
||||||
|
|
||||||
|
if (null !== $row['userIds']) {
|
||||||
|
foreach ($row['userIds'] as $id) {
|
||||||
|
$activity['users'][] = $this->normalizer->normalize(
|
||||||
|
$this->userRepository->find($id),
|
||||||
|
'docgen',
|
||||||
|
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => User::class]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$activity['users'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $row['personIds']) {
|
||||||
|
foreach ($row['personIds'] as $id) {
|
||||||
|
$activity['persons'][] = $this->normalizer->normalize(
|
||||||
|
$this->personRepository->find($id),
|
||||||
|
'docgen',
|
||||||
|
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => Person::class]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$activity['persons'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $row['thirdPartyIds']) {
|
||||||
|
foreach ($row['thirdPartyIds'] as $id) {
|
||||||
|
$activity['thirdParties'][] = $this->normalizer->normalize(
|
||||||
|
$this->thirdPartyRepository->find($id),
|
||||||
|
'docgen',
|
||||||
|
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => ThirdParty::class]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$activity['thirdParties'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $row['socialActionIds']) {
|
||||||
|
foreach ($row['socialActionIds'] as $id) {
|
||||||
|
$activity['socialActions'][] = $this->normalizer->normalize(
|
||||||
|
$this->socialActionRepository->find($id),
|
||||||
|
'docgen',
|
||||||
|
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => SocialAction::class]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$activity['socialActions'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $row['socialIssueIds']) {
|
||||||
|
foreach ($row['socialIssueIds'] as $id) {
|
||||||
|
$activity['socialIssues'][] = $this->normalizer->normalize(
|
||||||
|
$this->socialIssueRepository->find($id),
|
||||||
|
'docgen',
|
||||||
|
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => SocialIssue::class]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$activity['socialIssues'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$results[] = $activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
}
|
114
src/Bundle/ChillActivityBundle/Tests/Entity/ActivityTest.php
Normal file
114
src/Bundle/ChillActivityBundle/Tests/Entity/ActivityTest.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\ActivityBundle\Tests\Entity;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Entity\Activity;
|
||||||
|
use Chill\PersonBundle\AccompanyingPeriod\SocialIssueConsistency\AccompanyingPeriodSocialIssueConsistencyEntityListener;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||||
|
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
||||||
|
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class ActivityTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testHierarchySocialActions(): void
|
||||||
|
{
|
||||||
|
$parent = new SocialAction();
|
||||||
|
$child = new SocialAction();
|
||||||
|
|
||||||
|
$parent->addChild($child);
|
||||||
|
$grandChild = new SocialAction();
|
||||||
|
$child->addChild($grandChild);
|
||||||
|
|
||||||
|
$activity = new Activity();
|
||||||
|
|
||||||
|
$activity->addSocialAction($parent);
|
||||||
|
|
||||||
|
$this->assertCount(1, $activity->getSocialActions());
|
||||||
|
$this->assertContains($parent, $activity->getSocialActions());
|
||||||
|
|
||||||
|
$activity->addSocialAction($grandChild);
|
||||||
|
|
||||||
|
$this->assertCount(1, $activity->getSocialActions());
|
||||||
|
$this->assertContains($grandChild, $activity->getSocialActions());
|
||||||
|
$this->assertNotContains($parent, $activity->getSocialActions());
|
||||||
|
|
||||||
|
$activity->addSocialAction($child);
|
||||||
|
|
||||||
|
$this->assertCount(1, $activity->getSocialActions());
|
||||||
|
$this->assertContains($grandChild, $activity->getSocialActions());
|
||||||
|
$this->assertNotContains($parent, $activity->getSocialActions());
|
||||||
|
$this->assertNotContains($child, $activity->getSocialActions());
|
||||||
|
|
||||||
|
$activity->addSocialAction($another = new SocialAction());
|
||||||
|
|
||||||
|
$this->assertCount(2, $activity->getSocialActions());
|
||||||
|
$this->assertContains($grandChild, $activity->getSocialActions());
|
||||||
|
$this->assertContains($another, $activity->getSocialActions());
|
||||||
|
$this->assertNotContains($parent, $activity->getSocialActions());
|
||||||
|
$this->assertNotContains($child, $activity->getSocialActions());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHierarchySocialIssues(): void
|
||||||
|
{
|
||||||
|
$listener = new AccompanyingPeriodSocialIssueConsistencyEntityListener();
|
||||||
|
$event = $this->prophesize(LifecycleEventArgs::class)->reveal();
|
||||||
|
|
||||||
|
$parent = new SocialIssue();
|
||||||
|
$child = new SocialIssue();
|
||||||
|
|
||||||
|
$parent->addChild($child);
|
||||||
|
$grandChild = new SocialIssue();
|
||||||
|
$child->addChild($grandChild);
|
||||||
|
|
||||||
|
$activity = new Activity();
|
||||||
|
$activity->setAccompanyingPeriod(new AccompanyingPeriod());
|
||||||
|
|
||||||
|
$activity->addSocialIssue($parent);
|
||||||
|
$listener->preUpdate($activity, $event);
|
||||||
|
|
||||||
|
$this->assertCount(1, $activity->getSocialIssues());
|
||||||
|
$this->assertContains($parent, $activity->getSocialIssues());
|
||||||
|
|
||||||
|
$activity->addSocialIssue($grandChild);
|
||||||
|
$listener->preUpdate($activity, $event);
|
||||||
|
|
||||||
|
$this->assertCount(1, $activity->getSocialIssues());
|
||||||
|
$this->assertContains($grandChild, $activity->getSocialIssues());
|
||||||
|
$this->assertNotContains($parent, $activity->getSocialIssues());
|
||||||
|
|
||||||
|
$activity->addSocialIssue($child);
|
||||||
|
$listener->preUpdate($activity, $event);
|
||||||
|
|
||||||
|
$this->assertCount(1, $activity->getSocialIssues());
|
||||||
|
$this->assertContains($grandChild, $activity->getSocialIssues());
|
||||||
|
$this->assertNotContains($parent, $activity->getSocialIssues());
|
||||||
|
$this->assertNotContains($child, $activity->getSocialIssues());
|
||||||
|
|
||||||
|
$activity->addSocialIssue($another = new SocialIssue());
|
||||||
|
$listener->preUpdate($activity, $event);
|
||||||
|
|
||||||
|
$this->assertCount(2, $activity->getSocialIssues());
|
||||||
|
$this->assertContains($grandChild, $activity->getSocialIssues());
|
||||||
|
$this->assertContains($another, $activity->getSocialIssues());
|
||||||
|
$this->assertNotContains($parent, $activity->getSocialIssues());
|
||||||
|
$this->assertNotContains($child, $activity->getSocialIssues());
|
||||||
|
}
|
||||||
|
}
|
@ -66,6 +66,14 @@ Choose a type: Choisir un type
|
|||||||
1 hour 30: 1 heure 30
|
1 hour 30: 1 heure 30
|
||||||
1 hour 45: 1 heure 45
|
1 hour 45: 1 heure 45
|
||||||
2 hours: 2 heures
|
2 hours: 2 heures
|
||||||
|
2 hours 15: 2 heures 15
|
||||||
|
2 hours 30: 2 heures 30
|
||||||
|
2 hours 45: 2 heures 45
|
||||||
|
3 hours: 3 heures
|
||||||
|
3 hours 30: 3 heures 30
|
||||||
|
4 hours: 4 heures
|
||||||
|
4 hours 30: 4 heures 30
|
||||||
|
5 hours: 5 heures
|
||||||
Concerned groups: Parties concernées
|
Concerned groups: Parties concernées
|
||||||
Persons in accompanying course: Usagers du parcours
|
Persons in accompanying course: Usagers du parcours
|
||||||
Third persons: Tiers non-pro.
|
Third persons: Tiers non-pro.
|
||||||
@ -232,3 +240,5 @@ This is the minimal activity data: Activité n°
|
|||||||
docgen:
|
docgen:
|
||||||
Activity basic: Echange
|
Activity basic: Echange
|
||||||
A basic context for activity: Contexte pour les échanges
|
A basic context for activity: Contexte pour les échanges
|
||||||
|
Accompanying period with a list of activities: Parcours d'accompagnement avec liste des échanges
|
||||||
|
Accompanying period with a list of activities description: Ce contexte reprend les informations du parcours, et tous les échanges pour un parcours. Les échanges ne sont pas filtrés.
|
||||||
|
@ -31,6 +31,10 @@ class Configuration implements ConfigurationInterface
|
|||||||
->isRequired()
|
->isRequired()
|
||||||
->defaultValue(
|
->defaultValue(
|
||||||
[
|
[
|
||||||
|
['label' => '1 minutes', 'seconds' => 60],
|
||||||
|
['label' => '2 minutes', 'seconds' => 120],
|
||||||
|
['label' => '3 minutes', 'seconds' => 180],
|
||||||
|
['label' => '4 minutes', 'seconds' => 240],
|
||||||
['label' => '5 minutes', 'seconds' => 300],
|
['label' => '5 minutes', 'seconds' => 300],
|
||||||
['label' => '10 minutes', 'seconds' => 600],
|
['label' => '10 minutes', 'seconds' => 600],
|
||||||
['label' => '15 minutes', 'seconds' => 900],
|
['label' => '15 minutes', 'seconds' => 900],
|
||||||
|
@ -25,13 +25,13 @@ use function count;
|
|||||||
*/
|
*/
|
||||||
class SummaryBudget implements SummaryBudgetInterface
|
class SummaryBudget implements SummaryBudgetInterface
|
||||||
{
|
{
|
||||||
private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, type FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
||||||
|
|
||||||
private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, type FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
||||||
|
|
||||||
private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, type FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
||||||
|
|
||||||
private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, type FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
||||||
|
|
||||||
private array $chargeLabels;
|
private array $chargeLabels;
|
||||||
|
|
||||||
@ -110,7 +110,8 @@ class SummaryBudget implements SummaryBudgetInterface
|
|||||||
$rsm = new ResultSetMapping();
|
$rsm = new ResultSetMapping();
|
||||||
$rsm
|
$rsm
|
||||||
->addScalarResult('sum', 'sum')
|
->addScalarResult('sum', 'sum')
|
||||||
->addScalarResult('type', 'type');
|
->addScalarResult('type', 'type')
|
||||||
|
->addScalarResult('comment', 'comment');
|
||||||
|
|
||||||
return $rsm;
|
return $rsm;
|
||||||
}
|
}
|
||||||
@ -121,7 +122,7 @@ class SummaryBudget implements SummaryBudgetInterface
|
|||||||
$labels = $this->chargeLabels;
|
$labels = $this->chargeLabels;
|
||||||
|
|
||||||
return array_combine($keys, array_map(function ($i) use ($labels) {
|
return array_combine($keys, array_map(function ($i) use ($labels) {
|
||||||
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i])];
|
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => ''];
|
||||||
}, $keys));
|
}, $keys));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +132,7 @@ class SummaryBudget implements SummaryBudgetInterface
|
|||||||
$labels = $this->resourcesLabels;
|
$labels = $this->resourcesLabels;
|
||||||
|
|
||||||
return array_combine($keys, array_map(function ($i) use ($labels) {
|
return array_combine($keys, array_map(function ($i) use ($labels) {
|
||||||
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i])];
|
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => ''];
|
||||||
}, $keys));
|
}, $keys));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +159,7 @@ class SummaryBudget implements SummaryBudgetInterface
|
|||||||
$result[$row['type']] = [
|
$result[$row['type']] = [
|
||||||
'sum' => (float) $row['sum'],
|
'sum' => (float) $row['sum'],
|
||||||
'label' => $this->translatableStringHelper->localize($label[$row['type']]),
|
'label' => $this->translatableStringHelper->localize($label[$row['type']]),
|
||||||
|
'comment' => (string) $row['comment'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ namespace Chill\DocGeneratorBundle\Context;
|
|||||||
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
||||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
|
||||||
class ContextManager
|
final class ContextManager implements ContextManagerInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var DocGeneratorContextInterface[]|iterable
|
* @var DocGeneratorContextInterface[]|iterable
|
||||||
@ -26,9 +26,6 @@ class ContextManager
|
|||||||
$this->contexts = $contexts;
|
$this->contexts = $contexts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throw ContextNotFoundException when the context is not found
|
|
||||||
*/
|
|
||||||
public function getContextByDocGeneratorTemplate(DocGeneratorTemplate $docGeneratorTemplate): DocGeneratorContextInterface
|
public function getContextByDocGeneratorTemplate(DocGeneratorTemplate $docGeneratorTemplate): DocGeneratorContextInterface
|
||||||
{
|
{
|
||||||
foreach ($this->contexts as $key => $context) {
|
foreach ($this->contexts as $key => $context) {
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Context;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
|
||||||
|
interface ContextManagerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws ContextNotFoundException When the context is not found.
|
||||||
|
*/
|
||||||
|
public function getContextByDocGeneratorTemplate(DocGeneratorTemplate $docGeneratorTemplate): DocGeneratorContextInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContextNotFoundException When the context is not found.
|
||||||
|
*/
|
||||||
|
public function getContextByKey(string $searchedKey): DocGeneratorContextInterface;
|
||||||
|
|
||||||
|
public function getContexts(): array;
|
||||||
|
}
|
@ -11,8 +11,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\DocGeneratorBundle\Controller;
|
namespace Chill\DocGeneratorBundle\Controller;
|
||||||
|
|
||||||
use Base64Url\Base64Url;
|
|
||||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
|
||||||
use Chill\DocGeneratorBundle\Context\ContextManager;
|
use Chill\DocGeneratorBundle\Context\ContextManager;
|
||||||
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
||||||
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
||||||
@ -21,11 +19,11 @@ use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
|||||||
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
||||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
use Chill\MainBundle\Serializer\Model\Collection;
|
use Chill\MainBundle\Serializer\Model\Collection;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Exception;
|
use Exception;
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use GuzzleHttp\Exception\TransferException;
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
@ -35,14 +33,14 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
// TODO à mettre dans services
|
// TODO à mettre dans services
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Symfony\Component\HttpKernel\KernelInterface;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Throwable;
|
||||||
|
use function strlen;
|
||||||
|
|
||||||
final class DocGeneratorTemplateController extends AbstractController
|
final class DocGeneratorTemplateController extends AbstractController
|
||||||
{
|
{
|
||||||
@ -54,13 +52,13 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
|
|
||||||
private DriverInterface $driver;
|
private DriverInterface $driver;
|
||||||
|
|
||||||
private KernelInterface $kernel;
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
private LoggerInterface $logger;
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
private PaginatorFactory $paginatorFactory;
|
private PaginatorFactory $paginatorFactory;
|
||||||
|
|
||||||
private TempUrlGeneratorInterface $tempUrlGenerator;
|
private StoredObjectManagerInterface $storedObjectManager;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ContextManager $contextManager,
|
ContextManager $contextManager,
|
||||||
@ -68,18 +66,18 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
DriverInterface $driver,
|
DriverInterface $driver,
|
||||||
LoggerInterface $logger,
|
LoggerInterface $logger,
|
||||||
PaginatorFactory $paginatorFactory,
|
PaginatorFactory $paginatorFactory,
|
||||||
TempUrlGeneratorInterface $tempUrlGenerator,
|
HttpClientInterface $client,
|
||||||
KernelInterface $kernel,
|
StoredObjectManagerInterface $storedObjectManager,
|
||||||
HttpClientInterface $client
|
EntityManagerInterface $entityManager
|
||||||
) {
|
) {
|
||||||
$this->contextManager = $contextManager;
|
$this->contextManager = $contextManager;
|
||||||
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
|
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
|
||||||
$this->driver = $driver;
|
$this->driver = $driver;
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->paginatorFactory = $paginatorFactory;
|
$this->paginatorFactory = $paginatorFactory;
|
||||||
$this->tempUrlGenerator = $tempUrlGenerator;
|
|
||||||
$this->kernel = $kernel;
|
|
||||||
$this->client = $client;
|
$this->client = $client;
|
||||||
|
$this->storedObjectManager = $storedObjectManager;
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -178,8 +176,10 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
|
|
||||||
return $this->redirectToRoute(
|
return $this->redirectToRoute(
|
||||||
'chill_docgenerator_test_generate_from_template',
|
'chill_docgenerator_test_generate_from_template',
|
||||||
['template' => $template, 'entityClassName' => $entityClassName, 'entityId' => $entityId,
|
[
|
||||||
'returnPath' => $request->query->get('returnPath', '/'), ]
|
'template' => $template, 'entityClassName' => $entityClassName, 'entityId' => $entityId,
|
||||||
|
'returnPath' => $request->query->get('returnPath', '/'),
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,16 +193,26 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
try {
|
try {
|
||||||
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
||||||
} catch (ContextNotFoundException $e) {
|
} catch (ContextNotFoundException $e) {
|
||||||
throw new NotFoundHttpException($e->getMessage(), $e);
|
throw new NotFoundHttpException(
|
||||||
|
'Context not found.',
|
||||||
|
$e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$entity = $this->getDoctrine()->getRepository($context->getEntityClass())->find($entityId);
|
$entity = $this
|
||||||
|
->entityManager
|
||||||
|
->getRepository($context->getEntityClass())
|
||||||
|
->find($entityId);
|
||||||
|
|
||||||
if (null === $entity) {
|
if (null === $entity) {
|
||||||
throw new NotFoundHttpException("Entity with classname {$entityClassName} and id {$entityId} is not found");
|
throw new NotFoundHttpException(
|
||||||
|
sprintf('Entity with classname %s and id %s is not found', $entityClassName, $entityId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$contextGenerationData = [];
|
$contextGenerationData = [
|
||||||
|
'test_file' => null,
|
||||||
|
];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$context instanceof DocGeneratorContextWithPublicFormInterface
|
$context instanceof DocGeneratorContextWithPublicFormInterface
|
||||||
@ -240,128 +250,114 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
$contextGenerationData = $form->getData();
|
$contextGenerationData = $form->getData();
|
||||||
} elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) {
|
} elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) {
|
||||||
$templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig';
|
$templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig';
|
||||||
$templateOptions = ['entity' => $entity, 'form' => $form->createView(),
|
$templateOptions = [
|
||||||
'template' => $template, 'context' => $context, ];
|
'entity' => $entity, 'form' => $form->createView(),
|
||||||
|
'template' => $template, 'context' => $context,
|
||||||
|
];
|
||||||
|
|
||||||
return $this->render($templatePath, $templateOptions);
|
return $this->render($templatePath, $templateOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isTest && null !== $contextGenerationData['test_file']) {
|
$document = $template->getFile();
|
||||||
/** @var File $file */
|
|
||||||
$file = $contextGenerationData['test_file'];
|
if ($isTest && ($contextGenerationData['test_file'] instanceof File)) {
|
||||||
$templateResource = fopen($file->getPathname(), 'rb');
|
$dataDecrypted = file_get_contents($contextGenerationData['test_file']->getPathname());
|
||||||
} else {
|
} else {
|
||||||
$getUrlGen = $this->tempUrlGenerator->generate(
|
try {
|
||||||
'GET',
|
$dataDecrypted = $this->storedObjectManager->read($document);
|
||||||
$template->getFile()->getFilename()
|
} catch (Throwable $exception) {
|
||||||
);
|
throw $exception;
|
||||||
|
|
||||||
$data = $this->client->request('GET', $getUrlGen->url);
|
|
||||||
|
|
||||||
$iv = $template->getFile()->getIv(); // iv as an Array
|
|
||||||
$ivGoodFormat = pack('C*', ...$iv); // iv as a String (ok for openssl_decrypt)
|
|
||||||
|
|
||||||
$method = 'AES-256-CBC';
|
|
||||||
|
|
||||||
$key = $template->getFile()->getKeyInfos()['k'];
|
|
||||||
$keyGoodFormat = Base64Url::decode($key);
|
|
||||||
|
|
||||||
$dataDecrypted = openssl_decrypt($data->getContent(), $method, $keyGoodFormat, 1, $ivGoodFormat);
|
|
||||||
|
|
||||||
if (false === $dataDecrypted) {
|
|
||||||
throw new Exception('Error during Decrypt ', 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === $templateResource = fopen('php://memory', 'r+b')) {
|
|
||||||
$this->logger->error('Could not write data to memory');
|
|
||||||
|
|
||||||
throw new HttpException(500);
|
|
||||||
}
|
|
||||||
fwrite($templateResource, $dataDecrypted);
|
|
||||||
rewind($templateResource);
|
|
||||||
}
|
}
|
||||||
$datas = $context->getData($template, $entity, $contextGenerationData);
|
|
||||||
|
|
||||||
if ($isTest && isset($form) && $form['show_data']->getData()) {
|
if ($isTest && isset($form) && $form['show_data']->getData()) {
|
||||||
// very ugly hack...
|
// very ugly hack...
|
||||||
dd($datas);
|
dd($context->getData($template, $entity, $contextGenerationData));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$generatedResource = $this->driver->generateFromResource($templateResource, $template->getFile()->getType(), $datas, $template->getFile()->getFilename());
|
$generatedResource = $this
|
||||||
|
->driver
|
||||||
|
->generateFromString(
|
||||||
|
$dataDecrypted,
|
||||||
|
$template->getFile()->getType(),
|
||||||
|
$context->getData($template, $entity, $contextGenerationData),
|
||||||
|
$template->getFile()->getFilename()
|
||||||
|
);
|
||||||
} catch (TemplateException $e) {
|
} catch (TemplateException $e) {
|
||||||
$msg = implode("\n", $e->getErrors());
|
return new Response(
|
||||||
|
implode("\n", $e->getErrors()),
|
||||||
return new Response($msg, 400, [
|
400,
|
||||||
'Content-Type' => 'text/plain',
|
[
|
||||||
]);
|
'Content-Type' => 'text/plain',
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose($templateResource);
|
|
||||||
|
|
||||||
if ($isTest) {
|
if ($isTest) {
|
||||||
return new StreamedResponse(
|
return new Response(
|
||||||
static function () use ($generatedResource) {
|
$generatedResource,
|
||||||
fpassthru($generatedResource);
|
|
||||||
fclose($generatedResource);
|
|
||||||
},
|
|
||||||
Response::HTTP_OK,
|
Response::HTTP_OK,
|
||||||
[
|
[
|
||||||
'Content-Transfer-Encoding', 'binary',
|
'Content-Transfer-Encoding', 'binary',
|
||||||
'Content-Type' => 'application/vnd.oasis.opendocument.text',
|
'Content-Type' => 'application/vnd.oasis.opendocument.text',
|
||||||
'Content-Disposition' => sprintf('attachment; filename="%s.odt"', 'generated'),
|
'Content-Disposition' => 'attachment; filename="generated.odt"',
|
||||||
'Content-Length' => fstat($generatedResource)['size'],
|
'Content-Length' => strlen($generatedResource),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$genDocName = 'doc_' . sprintf('%010d', mt_rand()) . 'odt';
|
/** @var StoredObject $storedObject */
|
||||||
|
$storedObject = (new ObjectNormalizer())
|
||||||
$getUrlGen = $this->tempUrlGenerator->generate(
|
->denormalize(
|
||||||
'PUT',
|
[
|
||||||
$genDocName
|
'type' => $template->getFile()->getType(),
|
||||||
);
|
'filename' => sprintf('%s_odt', uniqid('doc_', true)),
|
||||||
|
],
|
||||||
$client = new Client();
|
StoredObject::class
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$putResponse = $client->request('PUT', $getUrlGen->url, [
|
$this->storedObjectManager->write($storedObject, $generatedResource);
|
||||||
'body' => $generatedResource,
|
} catch (Throwable $exception) {
|
||||||
]);
|
throw $exception;
|
||||||
|
}
|
||||||
|
|
||||||
if ($putResponse->getStatusCode() === 201) {
|
$this->entityManager->persist($storedObject);
|
||||||
$em = $this->getDoctrine()->getManager();
|
|
||||||
$storedObject = new StoredObject();
|
|
||||||
$storedObject
|
|
||||||
->setType($template->getFile()->getType())
|
|
||||||
->setFilename($genDocName);
|
|
||||||
|
|
||||||
$em->persist($storedObject);
|
try {
|
||||||
|
$context
|
||||||
try {
|
->storeGenerated(
|
||||||
$context->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
|
$template,
|
||||||
} catch (Exception $e) {
|
$storedObject,
|
||||||
$this->logger->error('Could not store the associated document to entity', [
|
$entity,
|
||||||
|
$contextGenerationData
|
||||||
|
);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this
|
||||||
|
->logger
|
||||||
|
->error(
|
||||||
|
'Unable to store the associated document to entity',
|
||||||
|
[
|
||||||
'entityClassName' => $entityClassName,
|
'entityClassName' => $entityClassName,
|
||||||
'entityId' => $entityId,
|
'entityId' => $entityId,
|
||||||
'contextKey' => $context->getName(),
|
'contextKey' => $context->getName(),
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
$em->flush();
|
|
||||||
|
|
||||||
return $this->redirectToRoute('chill_wopi_file_edit', [
|
|
||||||
'fileId' => $storedObject->getUuid(),
|
|
||||||
'returnPath' => $request->query->get('returnPath', '/'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} catch (TransferException $e) {
|
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Exception('Unable to generate document.');
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this
|
||||||
|
->redirectToRoute(
|
||||||
|
'chill_wopi_file_edit',
|
||||||
|
[
|
||||||
|
'fileId' => $storedObject->getUuid(),
|
||||||
|
'returnPath' => $request->query->get('returnPath', '/'),
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,5 @@ namespace Chill\DocGeneratorBundle\GeneratorDriver;
|
|||||||
|
|
||||||
interface DriverInterface
|
interface DriverInterface
|
||||||
{
|
{
|
||||||
/**
|
public function generateFromString(string $template, string $resourceType, array $data, ?string $templateName = null): string;
|
||||||
* @param resource $template
|
|
||||||
*
|
|
||||||
* @return resource
|
|
||||||
*/
|
|
||||||
public function generateFromResource($template, string $resourceType, array $data, ?string $templateName = null);
|
|
||||||
}
|
}
|
||||||
|
@ -16,45 +16,47 @@ use Psr\Log\LoggerInterface;
|
|||||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
use Symfony\Component\Mime\Part\DataPart;
|
use Symfony\Component\Mime\Part\DataPart;
|
||||||
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
|
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
|
||||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class RelatorioDriver implements DriverInterface
|
final class RelatorioDriver implements DriverInterface
|
||||||
{
|
{
|
||||||
private LoggerInterface $logger;
|
private HttpClientInterface $client;
|
||||||
|
|
||||||
private HttpClientInterface $relatorioClient;
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
private string $url;
|
private string $url;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
HttpClientInterface $relatorioClient,
|
HttpClientInterface $client,
|
||||||
ParameterBagInterface $parameterBag,
|
ParameterBagInterface $parameterBag,
|
||||||
LoggerInterface $logger
|
LoggerInterface $logger
|
||||||
) {
|
) {
|
||||||
$this->relatorioClient = $relatorioClient;
|
$this->client = $client;
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url'];
|
$this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function generateFromResource($template, string $resourceType, array $data, ?string $templateName = null)
|
public function generateFromString(string $template, string $resourceType, array $data, ?string $templateName = null): string
|
||||||
{
|
{
|
||||||
$formFields = [
|
$form = new FormDataPart(
|
||||||
'variables' => json_encode($data),
|
[
|
||||||
'template' => new DataPart($template, $templateName ?? uniqid('template_'), $resourceType),
|
'variables' => json_encode($data),
|
||||||
];
|
'template' => new DataPart($template, $templateName ?? uniqid('template_'), $resourceType),
|
||||||
$form = new FormDataPart($formFields);
|
]
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $this->relatorioClient->request('POST', $this->url, [
|
$response = $this->client->request('POST', $this->url, [
|
||||||
'headers' => $form->getPreparedHeaders()->toArray(),
|
'headers' => $form->getPreparedHeaders()->toArray(),
|
||||||
'body' => $form->bodyToIterable(),
|
'body' => $form->bodyToIterable(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $response->toStream();
|
return $response->getContent();
|
||||||
} catch (HttpExceptionInterface $e) {
|
} catch (ClientExceptionInterface $e) {
|
||||||
$content = $e->getResponse()->getContent(false);
|
$content = $e->getResponse()->getContent(false);
|
||||||
|
|
||||||
if (400 === $e->getResponse()->getStatusCode()) {
|
if (400 === $e->getResponse()->getStatusCode()) {
|
||||||
@ -87,6 +89,18 @@ class RelatorioDriver implements DriverInterface
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$this
|
||||||
|
->logger
|
||||||
|
->error(
|
||||||
|
'relatorio: Unable to get content from response.',
|
||||||
|
[
|
||||||
|
'msg' => $exception->getMessage(),
|
||||||
|
'e' => $exception->getTraceAsString(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
throw $exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
|
"spomky-labs/base64url": "^2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
},
|
},
|
||||||
|
@ -155,7 +155,7 @@ class DocumentAccompanyingCourseController extends AbstractController
|
|||||||
$documents = $this->courseRepository
|
$documents = $this->courseRepository
|
||||||
->findBy(
|
->findBy(
|
||||||
['course' => $course],
|
['course' => $course],
|
||||||
['date' => 'DESC'],
|
['date' => 'DESC', 'id' => 'DESC'],
|
||||||
$pagination->getItemsPerPage(),
|
$pagination->getItemsPerPage(),
|
||||||
$pagination->getCurrentPageFirstItemNumber()
|
$pagination->getCurrentPageFirstItemNumber()
|
||||||
);
|
);
|
||||||
|
@ -178,7 +178,7 @@ class DocumentPersonController extends AbstractController
|
|||||||
|
|
||||||
$documents = $this->personDocumentACLAwareRepository->findByPerson(
|
$documents = $this->personDocumentACLAwareRepository->findByPerson(
|
||||||
$person,
|
$person,
|
||||||
[],
|
['date' => 'DESC', 'id' => 'DESC'],
|
||||||
$pagination->getItemsPerPage(),
|
$pagination->getItemsPerPage(),
|
||||||
$pagination->getCurrentPageFirstItemNumber()
|
$pagination->getCurrentPageFirstItemNumber()
|
||||||
);
|
);
|
||||||
|
@ -109,12 +109,12 @@ class StoredObject implements AsyncFileInterface, Document
|
|||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getIv()
|
public function getIv(): array
|
||||||
{
|
{
|
||||||
return $this->iv;
|
return $this->iv;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getKeyInfos()
|
public function getKeyInfos(): array
|
||||||
{
|
{
|
||||||
return $this->keyInfos;
|
return $this->keyInfos;
|
||||||
}
|
}
|
||||||
@ -149,37 +149,37 @@ class StoredObject implements AsyncFileInterface, Document
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setDatas(array $datas)
|
public function setDatas(?array $datas)
|
||||||
{
|
{
|
||||||
$this->datas = $datas;
|
$this->datas = (array) $datas;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setFilename($filename)
|
public function setFilename(?string $filename)
|
||||||
{
|
{
|
||||||
$this->filename = $filename;
|
$this->filename = (string) $filename;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setIv($iv)
|
public function setIv(?array $iv)
|
||||||
{
|
{
|
||||||
$this->iv = $iv;
|
$this->iv = (array) $iv;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setKeyInfos($keyInfos)
|
public function setKeyInfos(?array $keyInfos)
|
||||||
{
|
{
|
||||||
$this->keyInfos = $keyInfos;
|
$this->keyInfos = (array) $keyInfos;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setType($type)
|
public function setType(?string $type)
|
||||||
{
|
{
|
||||||
$this->type = $type;
|
$this->type = (string) $type;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class StoredObjectManagerException extends Exception
|
||||||
|
{
|
||||||
|
public static function errorDuringHttpRequest(Throwable $exception): self
|
||||||
|
{
|
||||||
|
return new self('Error during HTTP request.', 500, $exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function invalidStatusCode(int $code): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
sprintf('Invalid status code received (%s).', $code)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function unableToDecrypt(string $message): self
|
||||||
|
{
|
||||||
|
return new self(sprintf('Unable to decrypt content (reason: %s).', $message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function unableToGetResponseContent(Throwable $exception): self
|
||||||
|
{
|
||||||
|
return new self('Unable to get content from response.', 500, $exception);
|
||||||
|
}
|
||||||
|
}
|
@ -64,7 +64,7 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito
|
|||||||
|
|
||||||
$this->addACL($qb, $person);
|
$this->addACL($qb, $person);
|
||||||
|
|
||||||
foreach ($orderBy as [$field, $order]) {
|
foreach ($orderBy as $field => $order) {
|
||||||
$qb->addOrderBy($field, $order);
|
$qb->addOrderBy($field, $order);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="create">
|
<li class="create">
|
||||||
<button class="btn btn-create">{{ 'Create'|trans }}</button>
|
<button class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
|
|
||||||
{# Twig way
|
|
||||||
TODO: une route, un template avec un header CHILL et un iframe
|
|
||||||
#}
|
|
||||||
<a href="{{ chill_path_add_return_path('chill_wopi_file_edit', {'fileId': document.uuid}) }}"
|
<a href="{{ chill_path_add_return_path('chill_wopi_file_edit', {'fileId': document.uuid}) }}"
|
||||||
class="btn btn-wopilink">
|
class="btn btn-wopilink">
|
||||||
{{ 'online_edit_document'|trans }}
|
{{ 'online_edit_document'|trans }}
|
||||||
|
@ -53,6 +53,11 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
|
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if chill_document_is_editable(document.object) %}
|
||||||
|
<li>
|
||||||
|
{{ document.object|chill_document_edit_button }}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
||||||
<li>
|
<li>
|
||||||
@ -75,6 +80,11 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a>
|
<a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if chill_document_is_editable(document.object) %}
|
||||||
|
<li>
|
||||||
|
{{ document.object|chill_document_edit_button }}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
|
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
|
||||||
<li>
|
<li>
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="create">
|
<li class="create">
|
||||||
<button class="btn btn-create">{{ 'Create'|trans }}</button>
|
<button class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
|
133
src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php
Normal file
133
src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Service;
|
||||||
|
|
||||||
|
use Base64Url\Base64Url;
|
||||||
|
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use const OPENSSL_RAW_DATA;
|
||||||
|
|
||||||
|
final class StoredObjectManager implements StoredObjectManagerInterface
|
||||||
|
{
|
||||||
|
private const ALGORITHM = 'AES-256-CBC';
|
||||||
|
|
||||||
|
private HttpClientInterface $client;
|
||||||
|
|
||||||
|
private TempUrlGeneratorInterface $tempUrlGenerator;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
HttpClientInterface $client,
|
||||||
|
TempUrlGeneratorInterface $tempUrlGenerator
|
||||||
|
) {
|
||||||
|
$this->client = $client;
|
||||||
|
$this->tempUrlGenerator = $tempUrlGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(StoredObject $document): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$response = $this
|
||||||
|
->client
|
||||||
|
->request(
|
||||||
|
Request::METHOD_GET,
|
||||||
|
$this
|
||||||
|
->tempUrlGenerator
|
||||||
|
->generate(
|
||||||
|
Request::METHOD_GET,
|
||||||
|
$document->getFilename()
|
||||||
|
)
|
||||||
|
->url
|
||||||
|
);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
throw StoredObjectManagerException::errorDuringHttpRequest($e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response->getStatusCode() !== Response::HTTP_OK) {
|
||||||
|
throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data = $response->getContent();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
throw StoredObjectManagerException::unableToGetResponseContent($e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $this->hasKeysAndIv($document)) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$clearData = openssl_decrypt(
|
||||||
|
$data,
|
||||||
|
self::ALGORITHM,
|
||||||
|
// TODO: Why using this library and not use base64_decode() ?
|
||||||
|
Base64Url::decode($document->getKeyInfos()['k']),
|
||||||
|
OPENSSL_RAW_DATA,
|
||||||
|
pack('C*', ...$document->getIv())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (false === $clearData) {
|
||||||
|
throw StoredObjectManagerException::unableToDecrypt(openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $clearData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write(StoredObject $document, string $clearContent): void
|
||||||
|
{
|
||||||
|
$encryptedContent = $this->hasKeysAndIv($document)
|
||||||
|
? openssl_encrypt(
|
||||||
|
$clearContent,
|
||||||
|
self::ALGORITHM,
|
||||||
|
// TODO: Why using this library and not use base64_decode() ?
|
||||||
|
Base64Url::decode($document->getKeyInfos()['k']),
|
||||||
|
OPENSSL_RAW_DATA,
|
||||||
|
pack('C*', ...$document->getIv())
|
||||||
|
)
|
||||||
|
: $clearContent;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this
|
||||||
|
->client
|
||||||
|
->request(
|
||||||
|
Request::METHOD_PUT,
|
||||||
|
$this
|
||||||
|
->tempUrlGenerator
|
||||||
|
->generate(
|
||||||
|
Request::METHOD_PUT,
|
||||||
|
$document->getFilename()
|
||||||
|
)
|
||||||
|
->url,
|
||||||
|
[
|
||||||
|
'body' => $encryptedContent,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (TransportExceptionInterface $exception) {
|
||||||
|
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response->getStatusCode() !== Response::HTTP_CREATED) {
|
||||||
|
throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasKeysAndIv(StoredObject $storedObject): bool
|
||||||
|
{
|
||||||
|
return ([] !== $storedObject->getKeyInfos()) && ([] !== $storedObject->getIv());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Service;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
|
||||||
|
interface StoredObjectManagerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the content of a StoredObject.
|
||||||
|
*
|
||||||
|
* @param StoredObject $document The document.
|
||||||
|
*
|
||||||
|
* @return string The retrieved content in clear.
|
||||||
|
*/
|
||||||
|
public function read(StoredObject $document): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the content of a StoredObject.
|
||||||
|
*
|
||||||
|
* @param StoredObject $document The document.
|
||||||
|
* @param $clearContent The content to store in clear.
|
||||||
|
*/
|
||||||
|
public function write(StoredObject $document, string $clearContent): void;
|
||||||
|
}
|
@ -16,10 +16,102 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
|||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Extension\RuntimeExtensionInterface;
|
use Twig\Extension\RuntimeExtensionInterface;
|
||||||
|
|
||||||
use function array_key_exists;
|
use function in_array;
|
||||||
|
|
||||||
final class WopiEditTwigExtensionRuntime implements RuntimeExtensionInterface
|
final class WopiEditTwigExtensionRuntime implements RuntimeExtensionInterface
|
||||||
{
|
{
|
||||||
|
public const SUPPORTED_MIMES = [
|
||||||
|
'image/svg+xml',
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.sun.xml.writer',
|
||||||
|
'application/vnd.oasis.opendocument.text',
|
||||||
|
'application/vnd.oasis.opendocument.text-flat-xml',
|
||||||
|
'application/vnd.sun.xml.calc',
|
||||||
|
'application/vnd.oasis.opendocument.spreadsheet',
|
||||||
|
'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
|
||||||
|
'application/vnd.sun.xml.impress',
|
||||||
|
'application/vnd.oasis.opendocument.presentation',
|
||||||
|
'application/vnd.oasis.opendocument.presentation-flat-xml',
|
||||||
|
'application/vnd.sun.xml.draw',
|
||||||
|
'application/vnd.oasis.opendocument.graphics',
|
||||||
|
'application/vnd.oasis.opendocument.graphics-flat-xml',
|
||||||
|
'application/vnd.oasis.opendocument.chart',
|
||||||
|
'application/vnd.sun.xml.writer.global',
|
||||||
|
'application/vnd.oasis.opendocument.text-master',
|
||||||
|
'application/vnd.sun.xml.writer.template',
|
||||||
|
'application/vnd.oasis.opendocument.text-template',
|
||||||
|
'application/vnd.oasis.opendocument.text-master-template',
|
||||||
|
'application/vnd.sun.xml.calc.template',
|
||||||
|
'application/vnd.oasis.opendocument.spreadsheet-template',
|
||||||
|
'application/vnd.sun.xml.impress.template',
|
||||||
|
'application/vnd.oasis.opendocument.presentation-template',
|
||||||
|
'application/vnd.sun.xml.draw.template',
|
||||||
|
'application/vnd.oasis.opendocument.graphics-template',
|
||||||
|
'application/msword',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.ms-word.document.macroEnabled.12',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
||||||
|
'application/vnd.ms-word.template.macroEnabled.12',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||||
|
'application/vnd.ms-excel.template.macroEnabled.12',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
|
||||||
|
'application/vnd.ms-excel.sheet.macroEnabled.12',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.template',
|
||||||
|
'application/vnd.ms-powerpoint.template.macroEnabled.12',
|
||||||
|
'application/vnd.wordperfect',
|
||||||
|
'application/x-aportisdoc',
|
||||||
|
'application/x-hwp',
|
||||||
|
'application/vnd.ms-works',
|
||||||
|
'application/x-mswrite',
|
||||||
|
'application/x-dif-document',
|
||||||
|
'text/spreadsheet',
|
||||||
|
'text/csv',
|
||||||
|
'application/x-dbase',
|
||||||
|
'application/vnd.lotus-1-2-3',
|
||||||
|
'image/cgm',
|
||||||
|
'image/vnd.dxf',
|
||||||
|
'image/x-emf',
|
||||||
|
'image/x-wmf',
|
||||||
|
'application/coreldraw',
|
||||||
|
'application/vnd.visio2013',
|
||||||
|
'application/vnd.visio',
|
||||||
|
'application/vnd.ms-visio.drawing',
|
||||||
|
'application/x-mspublisher',
|
||||||
|
'application/x-sony-bbeb',
|
||||||
|
'application/x-gnumeric',
|
||||||
|
'application/macwriteii',
|
||||||
|
'application/x-iwork-numbers-sffnumbers',
|
||||||
|
'application/vnd.oasis.opendocument.text-web',
|
||||||
|
'application/x-pagemaker',
|
||||||
|
'text/rtf',
|
||||||
|
'text/plain',
|
||||||
|
'application/x-fictionbook+xml',
|
||||||
|
'application/clarisworks',
|
||||||
|
'image/x-wpg',
|
||||||
|
'application/x-iwork-pages-sffpages',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
|
||||||
|
'application/x-iwork-keynote-sffkey',
|
||||||
|
|
||||||
|
'application/x-abiword',
|
||||||
|
'image/x-freehand',
|
||||||
|
'application/vnd.sun.xml.chart',
|
||||||
|
'application/x-t602',
|
||||||
|
'image/bmp',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'image/tiff',
|
||||||
|
'image/jpg',
|
||||||
|
'image/jpeg',
|
||||||
|
'application/pdf',
|
||||||
|
];
|
||||||
|
|
||||||
private const TEMPLATE = '@ChillDocStore/Button/wopi_edit_document.html.twig';
|
private const TEMPLATE = '@ChillDocStore/Button/wopi_edit_document.html.twig';
|
||||||
|
|
||||||
private DiscoveryInterface $discovery;
|
private DiscoveryInterface $discovery;
|
||||||
@ -31,15 +123,7 @@ final class WopiEditTwigExtensionRuntime implements RuntimeExtensionInterface
|
|||||||
|
|
||||||
public function isEditable(StoredObject $document): bool
|
public function isEditable(StoredObject $document): bool
|
||||||
{
|
{
|
||||||
$mime_type = $this->discovery->discoverMimeType($document->getType());
|
return in_array($document->getType(), self::SUPPORTED_MIMES, true);
|
||||||
|
|
||||||
foreach ($mime_type as $item) {
|
|
||||||
if (array_key_exists('default', $item) && 'true' === $item['default']) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string
|
public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string
|
||||||
|
183
src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php
Normal file
183
src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Tests;
|
||||||
|
|
||||||
|
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManager;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Generator;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use stdClass;
|
||||||
|
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @covers \Chill\DocStoreBundle\Service\StoredObjectManager
|
||||||
|
*/
|
||||||
|
final class StoredObjectManagerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function getDataProvider(): Generator
|
||||||
|
{
|
||||||
|
/* HAPPY SCENARIO */
|
||||||
|
|
||||||
|
// Encrypted object
|
||||||
|
yield [
|
||||||
|
(new StoredObject())
|
||||||
|
->setFilename('encrypted.txt')
|
||||||
|
->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')])
|
||||||
|
->setIv(unpack('C*', 'abcdefghijklmnop')),
|
||||||
|
hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string
|
||||||
|
'The quick brown fox jumps over the lazy dog', // clear
|
||||||
|
];
|
||||||
|
|
||||||
|
// Non-encrypted object
|
||||||
|
yield [
|
||||||
|
(new StoredObject())->setFilename('non-encrypted.txt'), // The StoredObject
|
||||||
|
'The quick brown fox jumps over the lazy dog', // Encrypted
|
||||||
|
'The quick brown fox jumps over the lazy dog', // Clear
|
||||||
|
];
|
||||||
|
|
||||||
|
/* UNHAPPY SCENARIO */
|
||||||
|
|
||||||
|
// Encrypted object with issue during HTTP communication
|
||||||
|
yield [
|
||||||
|
(new StoredObject())
|
||||||
|
->setFilename('error_during_http_request.txt')
|
||||||
|
->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')])
|
||||||
|
->setIv(unpack('C*', 'abcdefghijklmnop')),
|
||||||
|
hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string
|
||||||
|
'The quick brown fox jumps over the lazy dog', // clear
|
||||||
|
StoredObjectManagerException::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Encrypted object with issue during HTTP communication: Invalid status code
|
||||||
|
yield [
|
||||||
|
(new StoredObject())
|
||||||
|
->setFilename('invalid_statuscode.txt')
|
||||||
|
->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')])
|
||||||
|
->setIv(unpack('C*', 'abcdefghijklmnop')),
|
||||||
|
hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string
|
||||||
|
'The quick brown fox jumps over the lazy dog', // clear
|
||||||
|
StoredObjectManagerException::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Erroneous encrypted: Unable to decrypt exception.
|
||||||
|
yield [
|
||||||
|
(new StoredObject())
|
||||||
|
->setFilename('unable_to_decrypt.txt')
|
||||||
|
->setKeyInfos(['k' => base64_encode('WRONG_PASS_PHRASE')])
|
||||||
|
->setIv(unpack('C*', 'abcdefghijklmnop')),
|
||||||
|
'WRONG_ENCODED_VALUE', // Binary encoded string
|
||||||
|
'The quick brown fox jumps over the lazy dog', // clear
|
||||||
|
StoredObjectManagerException::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getDataProvider
|
||||||
|
*/
|
||||||
|
public function testRead(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null)
|
||||||
|
{
|
||||||
|
if (null !== $exceptionClass) {
|
||||||
|
$this->expectException($exceptionClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
$storedObjectManager = $this->getSubject($storedObject, $encodedContent);
|
||||||
|
|
||||||
|
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getDataProvider
|
||||||
|
*/
|
||||||
|
public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null)
|
||||||
|
{
|
||||||
|
if (null !== $exceptionClass) {
|
||||||
|
$this->expectException($exceptionClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
$storedObjectManager = $this->getSubject($storedObject, $encodedContent);
|
||||||
|
|
||||||
|
$storedObjectManager->write($storedObject, $clearContent);
|
||||||
|
|
||||||
|
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHttpClient(string $encodedContent): HttpClientInterface
|
||||||
|
{
|
||||||
|
$callback = static function ($method, $url, $options) use ($encodedContent) {
|
||||||
|
if (Request::METHOD_GET === $method) {
|
||||||
|
switch ($url) {
|
||||||
|
case 'https://example.com/non-encrypted.txt':
|
||||||
|
case 'https://example.com/encrypted.txt':
|
||||||
|
return new MockResponse($encodedContent, ['http_code' => 200]);
|
||||||
|
|
||||||
|
case 'https://example.com/error_during_http_request.txt':
|
||||||
|
return new TransportException('error_during_http_request.txt');
|
||||||
|
|
||||||
|
case 'https://example.com/invalid_statuscode.txt':
|
||||||
|
return new MockResponse($encodedContent, ['http_code' => 404]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Request::METHOD_PUT === $method) {
|
||||||
|
switch ($url) {
|
||||||
|
case 'https://example.com/non-encrypted.txt':
|
||||||
|
case 'https://example.com/encrypted.txt':
|
||||||
|
return new MockResponse($encodedContent, ['http_code' => 201]);
|
||||||
|
|
||||||
|
case 'https://example.com/error_during_http_request.txt':
|
||||||
|
throw new TransportException('error_during_http_request.txt');
|
||||||
|
|
||||||
|
case 'https://example.com/invalid_statuscode.txt':
|
||||||
|
return new MockResponse($encodedContent, ['http_code' => 404]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MockResponse('Not found');
|
||||||
|
};
|
||||||
|
|
||||||
|
return new MockHttpClient($callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSubject(StoredObject $storedObject, string $encodedContent): StoredObjectManagerInterface
|
||||||
|
{
|
||||||
|
return new StoredObjectManager(
|
||||||
|
$this->getHttpClient($encodedContent),
|
||||||
|
$this->getTempUrlGenerator($storedObject)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTempUrlGenerator(StoredObject $storedObject): TempUrlGeneratorInterface
|
||||||
|
{
|
||||||
|
$response = new stdClass();
|
||||||
|
$response->url = $storedObject->getFilename();
|
||||||
|
|
||||||
|
$tempUrlGenerator = $this
|
||||||
|
->getMockBuilder(TempUrlGeneratorInterface::class)
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$tempUrlGenerator
|
||||||
|
->method('generate')
|
||||||
|
->withAnyParameters()
|
||||||
|
->willReturn($response);
|
||||||
|
|
||||||
|
return $tempUrlGenerator;
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"symfony/mime": "^4 || ^5"
|
"symfony/mime": "^4 || ^5",
|
||||||
|
"symfony/http-foundation": "^4"
|
||||||
},
|
},
|
||||||
"license": "AGPL-3.0"
|
"license": "AGPL-3.0"
|
||||||
}
|
}
|
||||||
|
@ -40,3 +40,8 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: 'serializer.normalizer', priority: 16 }
|
- { name: 'serializer.normalizer', priority: 16 }
|
||||||
|
|
||||||
|
Chill\DocStoreBundle\Service\:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
resource: '../Service/'
|
||||||
|
|
||||||
|
@ -235,7 +235,7 @@ class ParticipationController extends AbstractController
|
|||||||
'The participation was created'
|
'The participation was created'
|
||||||
));
|
));
|
||||||
|
|
||||||
if ($request->query->get('return_path')) {
|
if ($request->query->has('return_path')) {
|
||||||
return $this->redirect($request->query->get('return_path'));
|
return $this->redirect($request->query->get('return_path'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -716,7 +716,7 @@ class ParticipationController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
protected function newSingle(Request $request)
|
protected function newSingle(Request $request)
|
||||||
{
|
{
|
||||||
$returnPath = $request->query->get('return_path') ?
|
$returnPath = $request->query->has('return_path') ?
|
||||||
$request->query->get('return_path') : null;
|
$request->query->get('return_path') : null;
|
||||||
|
|
||||||
$participation = $this->handleRequest($request, new Participation(), false);
|
$participation = $this->handleRequest($request, new Participation(), false);
|
||||||
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Controller;
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class LocationController extends CRUDController
|
class LocationController extends CRUDController
|
||||||
@ -29,4 +30,9 @@ class LocationController extends CRUDController
|
|||||||
{
|
{
|
||||||
$query->where('e.availableForUsers = true'); //TODO not working
|
$query->where('e.availableForUsers = true'); //TODO not working
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||||
|
{
|
||||||
|
return $query->addOrderBy('e.name', 'DESC');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
|
|||||||
use Chill\MainBundle\Repository\NotificationRepository;
|
use Chill\MainBundle\Repository\NotificationRepository;
|
||||||
use Chill\MainBundle\Security\Authorization\NotificationVoter;
|
use Chill\MainBundle\Security\Authorization\NotificationVoter;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@ -31,14 +32,19 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use function in_array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route("/{_locale}/notification")
|
* @Route("/{_locale}/notification")
|
||||||
*/
|
*/
|
||||||
class NotificationController extends AbstractController
|
class NotificationController extends AbstractController
|
||||||
{
|
{
|
||||||
|
private LoggerInterface $chillLogger;
|
||||||
|
|
||||||
private EntityManagerInterface $em;
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
private NotificationHandlerManager $notificationHandlerManager;
|
private NotificationHandlerManager $notificationHandlerManager;
|
||||||
|
|
||||||
private NotificationRepository $notificationRepository;
|
private NotificationRepository $notificationRepository;
|
||||||
@ -51,6 +57,8 @@ class NotificationController extends AbstractController
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
|
LoggerInterface $chillLogger,
|
||||||
|
LoggerInterface $logger,
|
||||||
Security $security,
|
Security $security,
|
||||||
NotificationRepository $notificationRepository,
|
NotificationRepository $notificationRepository,
|
||||||
NotificationHandlerManager $notificationHandlerManager,
|
NotificationHandlerManager $notificationHandlerManager,
|
||||||
@ -58,6 +66,8 @@ class NotificationController extends AbstractController
|
|||||||
TranslatorInterface $translator
|
TranslatorInterface $translator
|
||||||
) {
|
) {
|
||||||
$this->em = $em;
|
$this->em = $em;
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->chillLogger = $chillLogger;
|
||||||
$this->security = $security;
|
$this->security = $security;
|
||||||
$this->notificationRepository = $notificationRepository;
|
$this->notificationRepository = $notificationRepository;
|
||||||
$this->notificationHandlerManager = $notificationHandlerManager;
|
$this->notificationHandlerManager = $notificationHandlerManager;
|
||||||
@ -150,6 +160,52 @@ class NotificationController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/{id}/access_key", name="chill_main_notification_grant_access_by_access_key")
|
||||||
|
*/
|
||||||
|
public function getAccessByAccessKey(Notification $notification, Request $request): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||||
|
|
||||||
|
if (!$this->security->getUser() instanceof User) {
|
||||||
|
throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['accessKey'/*, 'email'*/] as $param) {
|
||||||
|
if (!$request->query->has($param)) {
|
||||||
|
throw new BadRequestHttpException("Missing {$param} parameter");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($notification->getAccessKey() !== $request->query->getAlnum('accessKey')) {
|
||||||
|
throw new AccessDeniedHttpException('access key is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
desactivated due to escaped '&' in email links
|
||||||
|
if (!in_array($request->query->get('email'), $notification->getAddressesEmails(), true)) {
|
||||||
|
return (new Response('The email address is no more associated with this notification'))
|
||||||
|
->setStatusCode(Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
$notification->addAddressee($this->security->getUser());
|
||||||
|
|
||||||
|
$this->getDoctrine()->getManager()->flush();
|
||||||
|
|
||||||
|
$logMsg = '[Notification] a user is granted access to notification trough an access key';
|
||||||
|
$context = [
|
||||||
|
'notificationId' => $notification->getId(),
|
||||||
|
'email' => $request->query->get('email'),
|
||||||
|
'user' => $this->security->getUser()->getId(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->logger->info($logMsg, $context);
|
||||||
|
$this->chillLogger->info($logMsg, $context);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('chill_main_notification_show', ['id' => $notification->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route("/inbox", name="chill_main_notification_my")
|
* @Route("/inbox", name="chill_main_notification_my")
|
||||||
*/
|
*/
|
||||||
|
@ -122,7 +122,7 @@ class SearchController extends AbstractController
|
|||||||
|
|
||||||
public function searchAction(Request $request, $_format)
|
public function searchAction(Request $request, $_format)
|
||||||
{
|
{
|
||||||
$pattern = $request->query->get('q', '');
|
$pattern = trim($request->query->get('q', ''));
|
||||||
|
|
||||||
if ('' === $pattern) {
|
if ('' === $pattern) {
|
||||||
switch ($_format) {
|
switch ($_format) {
|
||||||
|
@ -140,6 +140,7 @@ class Address
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
|
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
|
||||||
|
* @ORM\JoinColumn(nullable=false)
|
||||||
* @Groups({"write"})
|
* @Groups({"write"})
|
||||||
*/
|
*/
|
||||||
private ?PostalCode $postcode = null;
|
private ?PostalCode $postcode = null;
|
||||||
@ -480,8 +481,11 @@ class Address
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStreet(string $street): self
|
public function setStreet(?string $street): self
|
||||||
{
|
{
|
||||||
|
if (null === $street) {
|
||||||
|
$street = '';
|
||||||
|
}
|
||||||
$this->street = $street;
|
$this->street = $street;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -515,8 +519,11 @@ class Address
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStreetNumber(string $streetNumber): self
|
public function setStreetNumber(?string $streetNumber): self
|
||||||
{
|
{
|
||||||
|
if (null === $streetNumber) {
|
||||||
|
$streetNumber = '';
|
||||||
|
}
|
||||||
$this->streetNumber = $streetNumber;
|
$this->streetNumber = $streetNumber;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -18,6 +18,9 @@ use Doctrine\Common\Collections\ArrayCollection;
|
|||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
use function count;
|
||||||
|
use function in_array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Entity
|
* @ORM\Entity
|
||||||
@ -31,15 +34,34 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
*/
|
*/
|
||||||
class Notification implements TrackUpdateInterface
|
class Notification implements TrackUpdateInterface
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="text", nullable=false)
|
||||||
|
*/
|
||||||
|
private string $accessKey;
|
||||||
|
|
||||||
private array $addedAddresses = [];
|
private array $addedAddresses = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToMany(targetEntity=User::class)
|
* @ORM\ManyToMany(targetEntity=User::class)
|
||||||
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
|
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
|
||||||
* @Assert\Count(min="1", minMessage="notification.At least one addressee")
|
|
||||||
*/
|
*/
|
||||||
private Collection $addressees;
|
private Collection $addressees;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a list of destinee which will receive notifications.
|
||||||
|
*
|
||||||
|
* @var array|string[]
|
||||||
|
* @ORM\Column(type="json")
|
||||||
|
*/
|
||||||
|
private array $addressesEmails = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a list of emails adresses which were added to the notification.
|
||||||
|
*
|
||||||
|
* @var array|string[]
|
||||||
|
*/
|
||||||
|
private array $addressesEmailsAdded = [];
|
||||||
|
|
||||||
private ?ArrayCollection $addressesOnLoad = null;
|
private ?ArrayCollection $addressesOnLoad = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,6 +133,7 @@ class Notification implements TrackUpdateInterface
|
|||||||
$this->unreadBy = new ArrayCollection();
|
$this->unreadBy = new ArrayCollection();
|
||||||
$this->comments = new ArrayCollection();
|
$this->comments = new ArrayCollection();
|
||||||
$this->setDate(new DateTimeImmutable());
|
$this->setDate(new DateTimeImmutable());
|
||||||
|
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addAddressee(User $addressee): self
|
public function addAddressee(User $addressee): self
|
||||||
@ -123,6 +146,14 @@ class Notification implements TrackUpdateInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addAddressesEmail(string $email)
|
||||||
|
{
|
||||||
|
if (!in_array($email, $this->addressesEmails, true)) {
|
||||||
|
$this->addressesEmails[] = $email;
|
||||||
|
$this->addressesEmailsAdded[] = $email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function addComment(NotificationComment $comment): self
|
public function addComment(NotificationComment $comment): self
|
||||||
{
|
{
|
||||||
if (!$this->comments->contains($comment)) {
|
if (!$this->comments->contains($comment)) {
|
||||||
@ -142,6 +173,30 @@ class Notification implements TrackUpdateInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Assert\Callback
|
||||||
|
*
|
||||||
|
* @param array $payload
|
||||||
|
*/
|
||||||
|
public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
|
||||||
|
{
|
||||||
|
if (0 === (count($this->getAddressesEmails()) + count($this->getAddressees()))) {
|
||||||
|
$context->buildViolation('notification.At least one addressee')
|
||||||
|
->atPath('addressees')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAccessKey(): string
|
||||||
|
{
|
||||||
|
return $this->accessKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAddedAddresses(): array
|
||||||
|
{
|
||||||
|
return $this->addedAddresses;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection|User[]
|
* @return Collection|User[]
|
||||||
*/
|
*/
|
||||||
@ -155,6 +210,22 @@ class Notification implements TrackUpdateInterface
|
|||||||
return $this->addressees;
|
return $this->addressees;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|string[]
|
||||||
|
*/
|
||||||
|
public function getAddressesEmails(): array
|
||||||
|
{
|
||||||
|
return $this->addressesEmails;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|string[]
|
||||||
|
*/
|
||||||
|
public function getAddressesEmailsAdded(): array
|
||||||
|
{
|
||||||
|
return $this->addressesEmailsAdded;
|
||||||
|
}
|
||||||
|
|
||||||
public function getComments(): Collection
|
public function getComments(): Collection
|
||||||
{
|
{
|
||||||
return $this->comments;
|
return $this->comments;
|
||||||
@ -271,6 +342,14 @@ class Notification implements TrackUpdateInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function removeAddressesEmail(string $email)
|
||||||
|
{
|
||||||
|
if (in_array($email, $this->addressesEmails, true)) {
|
||||||
|
$this->addressesEmails = array_filter($this->addressesEmails, static fn ($e) => $e !== $email);
|
||||||
|
$this->addressesEmailsAdded = array_filter($this->addressesEmailsAdded, static fn ($e) => $e !== $email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function removeComment(NotificationComment $comment): self
|
public function removeComment(NotificationComment $comment): self
|
||||||
{
|
{
|
||||||
$this->comments->removeElement($comment);
|
$this->comments->removeElement($comment);
|
||||||
|
@ -12,12 +12,17 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Form;
|
namespace Chill\MainBundle\Form;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Notification;
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
||||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\Email;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotNull;
|
||||||
|
|
||||||
class NotificationType extends AbstractType
|
class NotificationType extends AbstractType
|
||||||
{
|
{
|
||||||
@ -30,9 +35,27 @@ class NotificationType extends AbstractType
|
|||||||
])
|
])
|
||||||
->add('addressees', PickUserDynamicType::class, [
|
->add('addressees', PickUserDynamicType::class, [
|
||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
|
'required' => false,
|
||||||
])
|
])
|
||||||
->add('message', ChillTextareaType::class, [
|
->add('message', ChillTextareaType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
])
|
||||||
|
->add('addressesEmails', ChillCollectionType::class, [
|
||||||
|
'label' => 'notification.dest by email',
|
||||||
|
'help' => 'notification.dest by email help',
|
||||||
|
'by_reference' => false,
|
||||||
|
'allow_add' => true,
|
||||||
|
'allow_delete' => true,
|
||||||
|
'entry_type' => EmailType::class,
|
||||||
|
'button_add_label' => 'notification.Add an email',
|
||||||
|
'button_remove_label' => 'notification.Remove an email',
|
||||||
|
'empty_collection_explain' => 'notification.Any email',
|
||||||
|
'entry_options' => [
|
||||||
|
'constraints' => [
|
||||||
|
new NotNull(), new NotBlank(), new Email(['checkMX' => true]),
|
||||||
|
],
|
||||||
|
'label' => 'Email',
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
37
src/Bundle/ChillMainBundle/Form/Type/AddressDateType.php
Normal file
37
src/Bundle/ChillMainBundle/Form/Type/AddressDateType.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Form\Type;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Address;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class AddressDateType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add(
|
||||||
|
'validFrom',
|
||||||
|
ChillDateType::class,
|
||||||
|
[
|
||||||
|
'required' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefault('data_class', Address::class);
|
||||||
|
}
|
||||||
|
}
|
@ -73,6 +73,17 @@ class NotificationMailer
|
|||||||
* Send a email after a notification is persisted.
|
* Send a email after a notification is persisted.
|
||||||
*/
|
*/
|
||||||
public function postPersistNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
|
public function postPersistNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
|
||||||
|
{
|
||||||
|
$this->sendNotificationEmailsToAddresses($notification);
|
||||||
|
$this->sendNotificationEmailsToAddressesEmails($notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postUpdateNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
|
||||||
|
{
|
||||||
|
$this->sendNotificationEmailsToAddressesEmails($notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendNotificationEmailsToAddresses(Notification $notification): void
|
||||||
{
|
{
|
||||||
foreach ($notification->getAddressees() as $addressee) {
|
foreach ($notification->getAddressees() as $addressee) {
|
||||||
if (null === $addressee->getEmail()) {
|
if (null === $addressee->getEmail()) {
|
||||||
@ -108,4 +119,31 @@ class NotificationMailer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function sendNotificationEmailsToAddressesEmails(Notification $notification): void
|
||||||
|
{
|
||||||
|
foreach ($notification->getAddressesEmailsAdded() as $emailAddress) {
|
||||||
|
$email = new TemplatedEmail();
|
||||||
|
$email
|
||||||
|
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')
|
||||||
|
->context([
|
||||||
|
'notification' => $notification,
|
||||||
|
'dest' => $emailAddress,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$email
|
||||||
|
->subject($notification->getTitle())
|
||||||
|
->to($emailAddress);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->mailer->send($email);
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
$this->logger->warning('[NotificationMailer] could not send an email notification', [
|
||||||
|
'to' => $emailAddress,
|
||||||
|
'error_message' => $e->getMessage(),
|
||||||
|
'error_trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification\EventListener;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Notification\NotificationPersisterInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\HttpKernel\Event\TerminateEvent;
|
||||||
|
use function count;
|
||||||
|
|
||||||
|
class PersistNotificationOnTerminateEventSubscriber implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
|
private NotificationPersisterInterface $persister;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $em, NotificationPersisterInterface $persister)
|
||||||
|
{
|
||||||
|
$this->em = $em;
|
||||||
|
$this->persister = $persister;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'kernel.terminate' => [
|
||||||
|
['onKernelTerminate', 1024], // we must ensure that the priority is before sending email
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onKernelTerminate(TerminateEvent $event): void
|
||||||
|
{
|
||||||
|
if ($event->isMasterRequest()) {
|
||||||
|
$this->persistNotifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistNotifications(): void
|
||||||
|
{
|
||||||
|
if (0 < count($this->persister->getWaitingNotifications())) {
|
||||||
|
foreach ($this->persister->getWaitingNotifications() as $notification) {
|
||||||
|
$this->em->persist($notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
|
||||||
|
class NotificationPersister implements NotificationPersisterInterface
|
||||||
|
{
|
||||||
|
private array $waitingNotifications = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|Notification[]
|
||||||
|
*/
|
||||||
|
public function getWaitingNotifications(): array
|
||||||
|
{
|
||||||
|
return $this->waitingNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function persist(Notification $notification): void
|
||||||
|
{
|
||||||
|
$this->waitingNotifications[] = $notification;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the notification.
|
||||||
|
*
|
||||||
|
* Those notification will be stored into database by the kernel. This
|
||||||
|
* will also ensure that this happens outside of regular operations
|
||||||
|
* operated by the entity manager.
|
||||||
|
*/
|
||||||
|
interface NotificationPersisterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array|Notification[]
|
||||||
|
*/
|
||||||
|
public function getWaitingNotifications(): array;
|
||||||
|
|
||||||
|
public function persist(Notification $notification): void;
|
||||||
|
}
|
@ -56,7 +56,7 @@ final class NotificationRepository implements ObjectRepository
|
|||||||
if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) {
|
if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) {
|
||||||
$sql =
|
$sql =
|
||||||
'SELECT
|
'SELECT
|
||||||
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau JOIN chill_main_notification cmn ON cmnau.notification_id = cmn.id WHERE user_id = :userid and cmnau.notification_id = cmn.id and cmn.sender_id IS NOT NULL))::int) AS unread,
|
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = :userid and cmnau.notification_id = cmn.id))::int) AS unread,
|
||||||
SUM((cmn.sender_id = :userid)::int) AS sent,
|
SUM((cmn.sender_id = :userid)::int) AS sent,
|
||||||
COUNT(cmn.*) AS total
|
COUNT(cmn.*) AS total
|
||||||
FROM chill_main_notification cmn
|
FROM chill_main_notification cmn
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
// Chill forms
|
// Chill forms
|
||||||
@import './scss/forms';
|
@import './scss/forms';
|
||||||
|
|
||||||
|
// Extend bootstrap accordion
|
||||||
|
@import './scss/accordion';
|
||||||
|
|
||||||
// Chill record_actions
|
// Chill record_actions
|
||||||
@import './scss/record_actions';
|
@import './scss/record_actions';
|
||||||
|
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
.accordion {
|
||||||
|
// show a folded / label on accordion
|
||||||
|
button[aria-expanded="true"] > span.folded,
|
||||||
|
button[aria-expanded="false"] > span.unfolded { display: none; }
|
||||||
|
button[aria-expanded="false"] > span.folded,
|
||||||
|
button[aria-expanded="true"] > span.unfolded { display: inline; }
|
||||||
|
|
||||||
|
.accordion-item {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ window.addEventListener('DOMContentLoaded', function (e) {
|
|||||||
buttonClass: el.dataset.buttonClass,
|
buttonClass: el.dataset.buttonClass,
|
||||||
buttonNoText: 'false' === el.dataset.buttonText,
|
buttonNoText: 'false' === el.dataset.buttonText,
|
||||||
showUrl: el.dataset.showButtonUrl,
|
showUrl: el.dataset.showButtonUrl,
|
||||||
isRead: 1 === el.dataset.notificationCurrentIsRead,
|
isRead: 1 === Number.parseInt(el.dataset.notificationCurrentIsRead),
|
||||||
container: el.dataset.container
|
container: el.dataset.container
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -25,14 +25,14 @@
|
|||||||
{{ $t('my_accompanying_courses.tab') }}
|
{{ $t('my_accompanying_courses.tab') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<!-- <li class="nav-item">
|
||||||
<a class="nav-link"
|
<a class="nav-link"
|
||||||
:class="{'active': activeTab === 'MyWorks'}"
|
:class="{'active': activeTab === 'MyWorks'}"
|
||||||
@click="selectTab('MyWorks')">
|
@click="selectTab('MyWorks')">
|
||||||
{{ $t('my_works.tab') }}
|
{{ $t('my_works.tab') }}
|
||||||
<tab-counter :count="state.works.count"></tab-counter>
|
<tab-counter :count="state.works.count"></tab-counter>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li> -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link"
|
<a class="nav-link"
|
||||||
:class="{'active': activeTab === 'MyEvaluations'}"
|
:class="{'active': activeTab === 'MyEvaluations'}"
|
||||||
@ -135,7 +135,7 @@ export default {
|
|||||||
for (const m of [
|
for (const m of [
|
||||||
'MyNotifications',
|
'MyNotifications',
|
||||||
'MyAccompanyingCourses',
|
'MyAccompanyingCourses',
|
||||||
'MyWorks',
|
// 'MyWorks',
|
||||||
'MyEvaluations',
|
'MyEvaluations',
|
||||||
'MyTasks',
|
'MyTasks',
|
||||||
'MyWorkflows',
|
'MyWorkflows',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// CURRENTLY NOT IN USE
|
||||||
<template>
|
<template>
|
||||||
<div class="accompanying_course_work">
|
<div class="accompanying_course_work">
|
||||||
<div class="alert alert-light">{{ $t('my_works.description') }}</div>
|
<div class="alert alert-light">{{ $t('my_works.description') }}</div>
|
||||||
|
@ -15,11 +15,11 @@ const appMessages = {
|
|||||||
description_warning: "Liste des tâches auxquelles je suis assigné et dont la date d'échéance est dépassée.",
|
description_warning: "Liste des tâches auxquelles je suis assigné et dont la date d'échéance est dépassée.",
|
||||||
},
|
},
|
||||||
my_accompanying_courses: {
|
my_accompanying_courses: {
|
||||||
tab: "Mes parcours",
|
tab: "Mes nouveaux parcours",
|
||||||
description: "Liste des parcours d'accompagnement que l'on vient de m'attribuer.",
|
description: "Liste des parcours d'accompagnement que l'on vient de m'attribuer depuis moins de 15 jours.",
|
||||||
},
|
},
|
||||||
my_notifications: {
|
my_notifications: {
|
||||||
tab: "Mes notifications",
|
tab: "Mes nouvelles notifications",
|
||||||
description: "Liste des notifications reçues et non lues.",
|
description: "Liste des notifications reçues et non lues.",
|
||||||
},
|
},
|
||||||
my_workflows: {
|
my_workflows: {
|
||||||
|
@ -13,7 +13,7 @@ const isEmpty = (obj) => {
|
|||||||
const store = createStore({
|
const store = createStore({
|
||||||
strict: debug,
|
strict: debug,
|
||||||
state: {
|
state: {
|
||||||
works: {},
|
// works: {},
|
||||||
evaluations: {},
|
evaluations: {},
|
||||||
tasks: {
|
tasks: {
|
||||||
warning: {},
|
warning: {},
|
||||||
@ -26,9 +26,9 @@ const store = createStore({
|
|||||||
loading: false
|
loading: false
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
isWorksLoaded(state) {
|
// isWorksLoaded(state) {
|
||||||
return !isEmpty(state.works);
|
// return !isEmpty(state.works);
|
||||||
},
|
// },
|
||||||
isEvaluationsLoaded(state) {
|
isEvaluationsLoaded(state) {
|
||||||
return !isEmpty(state.evaluations);
|
return !isEmpty(state.evaluations);
|
||||||
},
|
},
|
||||||
@ -49,7 +49,7 @@ const store = createStore({
|
|||||||
},
|
},
|
||||||
counter(state) {
|
counter(state) {
|
||||||
return {
|
return {
|
||||||
works: state.works.count,
|
// works: state.works.count,
|
||||||
evaluations: state.evaluations.count,
|
evaluations: state.evaluations.count,
|
||||||
tasksWarning: state.tasks.warning.count,
|
tasksWarning: state.tasks.warning.count,
|
||||||
tasksAlert: state.tasks.alert.count,
|
tasksAlert: state.tasks.alert.count,
|
||||||
@ -60,10 +60,10 @@ const store = createStore({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
addWorks(state, works) {
|
// addWorks(state, works) {
|
||||||
//console.log('addWorks', works);
|
// //console.log('addWorks', works);
|
||||||
state.works = works;
|
// state.works = works;
|
||||||
},
|
// },
|
||||||
addEvaluations(state, evaluations) {
|
addEvaluations(state, evaluations) {
|
||||||
//console.log('addEvaluations', evaluations);
|
//console.log('addEvaluations', evaluations);
|
||||||
state.evaluations = evaluations;
|
state.evaluations = evaluations;
|
||||||
@ -99,22 +99,22 @@ const store = createStore({
|
|||||||
switch (tab) {
|
switch (tab) {
|
||||||
case 'MyCustoms':
|
case 'MyCustoms':
|
||||||
break;
|
break;
|
||||||
case 'MyWorks':
|
// case 'MyWorks':
|
||||||
if (!getters.isWorksLoaded) {
|
// if (!getters.isWorksLoaded) {
|
||||||
commit('setLoading', true);
|
// commit('setLoading', true);
|
||||||
const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
|
// const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
|
||||||
makeFetch('GET', url)
|
// makeFetch('GET', url)
|
||||||
.then((response) => {
|
// .then((response) => {
|
||||||
commit('addWorks', response);
|
// commit('addWorks', response);
|
||||||
commit('setLoading', false);
|
// commit('setLoading', false);
|
||||||
})
|
// })
|
||||||
.catch((error) => {
|
// .catch((error) => {
|
||||||
commit('catchError', error);
|
// commit('catchError', error);
|
||||||
throw error;
|
// throw error;
|
||||||
})
|
// })
|
||||||
;
|
// ;
|
||||||
}
|
// }
|
||||||
break;
|
// break;
|
||||||
case 'MyEvaluations':
|
case 'MyEvaluations':
|
||||||
if (!getters.isEvaluationsLoaded) {
|
if (!getters.isEvaluationsLoaded) {
|
||||||
commit('setLoading', true);
|
commit('setLoading', true);
|
||||||
|
@ -47,6 +47,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{{ chill_pagination(paginator) }}
|
||||||
|
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_crud_main_location_new') }}" class="btn btn-create">
|
<a href="{{ path('chill_crud_main_location_new') }}" class="btn btn-create">
|
||||||
|
@ -40,6 +40,11 @@
|
|||||||
{{ a|chill_entity_render_string }}
|
{{ a|chill_entity_render_string }}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% for a in c.notification.addressesEmails %}
|
||||||
|
<span class="badge-user" title="{{ 'notification.Email with access link'|trans|e('html_attr') }}">
|
||||||
|
{{ a }}
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -61,17 +66,28 @@
|
|||||||
<div class="item-row">
|
<div class="item-row">
|
||||||
<div class="notification-content">
|
<div class="notification-content">
|
||||||
{% if c.full_content is defined and c.full_content == true %}
|
{% if c.full_content is defined and c.full_content == true %}
|
||||||
{{ c.notification.message|chill_markdown_to_html }}
|
{% if c.notification.message is not empty %}
|
||||||
|
{{ c.notification.message|chill_markdown_to_html }}
|
||||||
|
{% else %}
|
||||||
|
<p class="chill-no-data-statement">{{ 'Any comment'|trans }}</p>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
|
{% if c.notification.message is not empty %}
|
||||||
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p>
|
{{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
|
||||||
|
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p>
|
||||||
|
{% else %}
|
||||||
|
<p class="chill-no-data-statement">{{ 'Any comment'|trans }}</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro actions(c) %}
|
||||||
{% if c.action_button is not defined or c.action_button != false %}
|
{% if c.action_button is not defined or c.action_button != false %}
|
||||||
<div class="item-row separator">
|
<div class="item-row separator">
|
||||||
<div class="item-col item-meta">
|
<div class="item-col item-meta">
|
||||||
|
|
||||||
{% if c.notification.comments|length > 0 %}
|
{% if c.notification.comments|length > 0 %}
|
||||||
<div class="comment-counter">
|
<div class="comment-counter">
|
||||||
<span class="counter">
|
<span class="counter">
|
||||||
@ -79,16 +95,16 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="item-col">
|
<div class="item-col">
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
<li>
|
<li>
|
||||||
{# Vue component #}
|
{# Vue component #}
|
||||||
<span class="notification_toggle_read_status"
|
<span class="notification_toggle_read_status"
|
||||||
data-notification-id="{{ c.notification.id }}"
|
data-notification-id="{{ c.notification.id }}"
|
||||||
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
|
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
|
||||||
data-container="notification-status"
|
data-container="notification-status"
|
||||||
></span>
|
></span>
|
||||||
</li>
|
</li>
|
||||||
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %}
|
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %}
|
||||||
@ -122,24 +138,25 @@
|
|||||||
<button type="button" class="accordion-button collapsed"
|
<button type="button" class="accordion-button collapsed"
|
||||||
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}"
|
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}"
|
||||||
aria-expanded="false" aria-controls="flush-collapse-{{ notification.id }}">
|
aria-expanded="false" aria-controls="flush-collapse-{{ notification.id }}">
|
||||||
|
|
||||||
{{ _self.title(_context) }}
|
{{ _self.title(_context) }}
|
||||||
</button>
|
</button>
|
||||||
{{ _self.header(_context) }}
|
{{ _self.header(_context) }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div id="flush-collapse-{{ notification.id }}"
|
<div id="flush-collapse-{{ notification.id }}"
|
||||||
class="accordion-collapse collapse"
|
class="accordion-collapse collapse"
|
||||||
aria-labelledby="flush-heading-{{ notification.id }}"
|
aria-labelledby="flush-heading-{{ notification.id }}"
|
||||||
data-bs-parent="#notification-fold">
|
data-bs-parent="#notification-fold">
|
||||||
|
|
||||||
{{ _self.content(_context) }}
|
{{ _self.content(_context) }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
{{ _self.actions(_context) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ _self.title(_context) }}
|
{{ _self.title(_context) }}
|
||||||
{{ _self.header(_context) }}
|
{{ _self.header(_context) }}
|
||||||
{{ _self.content(_context) }}
|
{{ _self.content(_context) }}
|
||||||
|
{{ _self.actions(_context) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
|
|
||||||
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
||||||
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
||||||
|
|
||||||
|
{{ form_row(form.addressesEmails) }}
|
||||||
|
|
||||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||||
|
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
|
@ -21,6 +21,8 @@
|
|||||||
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
||||||
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
||||||
|
|
||||||
|
{{ form_row(form.addressesEmails) }}
|
||||||
|
|
||||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||||
|
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
{{ dest }},
|
||||||
|
|
||||||
|
{{ notification.sender.label }} a créé une notification pour vous:
|
||||||
|
|
||||||
|
> {{ notification.title }}
|
||||||
|
>
|
||||||
|
>
|
||||||
|
{%- for line in notification.message|split("\n") %}
|
||||||
|
> {{ line }}
|
||||||
|
{%- if not loop.last %}
|
||||||
|
>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
|
||||||
|
Vous pouvez cliquer sur ce lien pour obtenir un accès permanent à la notification:
|
||||||
|
|
||||||
|
{{ absolute_url(path('chill_main_notification_grant_access_by_access_key', {'_locale': 'fr', 'id': notification.id, 'accessKey': notification.accessKey, 'email': dest})) }}
|
||||||
|
|
||||||
|
--
|
||||||
|
Le logiciel Chill
|
@ -49,11 +49,12 @@ class SearchUserApiProvider implements SearchApiInterface
|
|||||||
SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical))', [$pattern, $pattern])
|
SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical))', [$pattern, $pattern])
|
||||||
->setFromClause('users AS u')
|
->setFromClause('users AS u')
|
||||||
->setWhereClauses('
|
->setWhereClauses('
|
||||||
|
u.enabled IS TRUE and (
|
||||||
SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15
|
SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15
|
||||||
OR u.usernamecanonical LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\'
|
OR u.usernamecanonical LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\'
|
||||||
OR SIMILARITY(LOWER(UNACCENT(?)), LOWER(UNACCENT(u.label))) > 0.15
|
OR SIMILARITY(LOWER(UNACCENT(?)), LOWER(UNACCENT(u.label))) > 0.15
|
||||||
OR u.label LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\'
|
OR u.label LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\'
|
||||||
', [$pattern, $pattern, $pattern, $pattern]);
|
)', [$pattern, $pattern, $pattern, $pattern]);
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,23 @@ class EntityWorkflowVoter extends Voter
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->security->isGranted($entityAttribute, $handler->getRelatedEntity($subject));
|
$relatedEntity = $handler->getRelatedEntity($subject);
|
||||||
|
|
||||||
|
if (null === $relatedEntity) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->security->isGranted($entityAttribute, $relatedEntity)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($subject->getSteps() as $step) {
|
||||||
|
if ($step->getAllDestUser()->contains($token->getUser())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
case self::DELETE:
|
case self::DELETE:
|
||||||
return $subject->getStep() === 'initial';
|
return $subject->getStep() === 'initial';
|
||||||
|
@ -88,6 +88,24 @@ final class NotificationTest extends KernelTestCase
|
|||||||
$this->assertNotContains($user1, $notification->getUnreadBy()->toArray());
|
$this->assertNotContains($user1, $notification->getUnreadBy()->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAddressesEmail(): void
|
||||||
|
{
|
||||||
|
$notification = new Notification();
|
||||||
|
|
||||||
|
$notification->addAddressesEmail('test');
|
||||||
|
$notification->addAddressesEmail('other');
|
||||||
|
|
||||||
|
$this->assertContains('test', $notification->getAddressesEmails());
|
||||||
|
$this->assertContains('other', $notification->getAddressesEmails());
|
||||||
|
$this->assertContains('test', $notification->getAddressesEmailsAdded());
|
||||||
|
$this->assertContains('other', $notification->getAddressesEmailsAdded());
|
||||||
|
|
||||||
|
$notification->removeAddressesEmail('other');
|
||||||
|
|
||||||
|
$this->assertNotContains('other', $notification->getAddressesEmails());
|
||||||
|
$this->assertNotContains('other', $notification->getAddressesEmailsAdded());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider generateNotificationData
|
* @dataProvider generateNotificationData
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Notification\EventListener;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Notification\EventListener\PersistNotificationOnTerminateEventSubscriber;
|
||||||
|
use Chill\MainBundle\Notification\NotificationPersister;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\HttpKernel\Event\TerminateEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class PersistNotificationOnTerminateEventSubscriberTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testNotificationIsPersisted()
|
||||||
|
{
|
||||||
|
$persister = new NotificationPersister();
|
||||||
|
$em = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$em->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1);
|
||||||
|
$em->flush()->shouldBeCalledTimes(1);
|
||||||
|
$event = $this->prophesize(TerminateEvent::class);
|
||||||
|
$event->isMasterRequest()->willReturn(true);
|
||||||
|
|
||||||
|
$eventSubscriber = new PersistNotificationOnTerminateEventSubscriber($em->reveal(), $persister);
|
||||||
|
|
||||||
|
$notification = new Notification();
|
||||||
|
$persister->persist($notification);
|
||||||
|
|
||||||
|
$eventSubscriber->onKernelTerminate($event->reveal());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Serializer\Normalizer\PhonenumberNormalizer;
|
||||||
|
use libphonenumber\PhoneNumber;
|
||||||
|
use libphonenumber\PhoneNumberUtil;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class PhonenumberNormalizerTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function dataProviderNormalizePhonenumber()
|
||||||
|
{
|
||||||
|
$phonenumberUtil = PhoneNumberUtil::getInstance();
|
||||||
|
|
||||||
|
yield [$phonenumberUtil->parse('+32486123465'), 'docgen', ['docgen:expects' => PhoneNumber::class], '0486 12 34 65'];
|
||||||
|
|
||||||
|
yield [null, 'docgen', ['docgen:expects' => PhoneNumber::class], ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderNormalizePhonenumber
|
||||||
|
*
|
||||||
|
* @param mixed $format
|
||||||
|
* @param mixed $context
|
||||||
|
* @param mixed $expected
|
||||||
|
*/
|
||||||
|
public function testNormalize(?Phonenumber $phonenumber, $format, $context, $expected)
|
||||||
|
{
|
||||||
|
$parameterBag = $this->prophesize(ParameterBagInterface::class);
|
||||||
|
$parameterBag->get(Argument::exact('chill_main'))->willReturn(['phone_helper' => ['default_carrier_code' => 'BE']]);
|
||||||
|
$normalizer = new PhonenumberNormalizer($parameterBag->reveal());
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $normalizer->normalize($phonenumber, $format, $context));
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,11 @@ services:
|
|||||||
autowire: true
|
autowire: true
|
||||||
autoconfigure: true
|
autoconfigure: true
|
||||||
|
|
||||||
|
Chill\MainBundle\Notification\:
|
||||||
|
resource: ../../Notification/
|
||||||
|
autoconfigure: true
|
||||||
|
autowire: true
|
||||||
|
|
||||||
Chill\MainBundle\Notification\Mailer:
|
Chill\MainBundle\Notification\Mailer:
|
||||||
arguments:
|
arguments:
|
||||||
$logger: '@Psr\Log\LoggerInterface'
|
$logger: '@Psr\Log\LoggerInterface'
|
||||||
@ -17,12 +22,6 @@ services:
|
|||||||
arguments:
|
arguments:
|
||||||
$handlers: !tagged_iterator chill_main.notification_handler
|
$handlers: !tagged_iterator chill_main.notification_handler
|
||||||
|
|
||||||
Chill\MainBundle\Notification\NotificationPresence: ~
|
|
||||||
|
|
||||||
Chill\MainBundle\Notification\Templating\NotificationTwigExtension: ~
|
|
||||||
|
|
||||||
Chill\MainBundle\Notification\Templating\NotificationTwigExtensionRuntime: ~
|
|
||||||
|
|
||||||
Chill\MainBundle\Notification\Counter\NotificationByUserCounter:
|
Chill\MainBundle\Notification\Counter\NotificationByUserCounter:
|
||||||
autoconfigure: true
|
autoconfigure: true
|
||||||
autowire: true
|
autowire: true
|
||||||
@ -61,6 +60,15 @@ services:
|
|||||||
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||||
lazy: true
|
lazy: true
|
||||||
method: 'postPersistNotification'
|
method: 'postPersistNotification'
|
||||||
|
|
||||||
|
-
|
||||||
|
name: 'doctrine.orm.entity_listener'
|
||||||
|
event: 'postUpdate'
|
||||||
|
entity: 'Chill\MainBundle\Entity\Notification'
|
||||||
|
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||||
|
lazy: true
|
||||||
|
method: 'postUpdateNotification'
|
||||||
|
|
||||||
-
|
-
|
||||||
name: 'doctrine.orm.entity_listener'
|
name: 'doctrine.orm.entity_listener'
|
||||||
event: 'postPersist'
|
event: 'postPersist'
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20220413154743 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification DROP adressesEmails');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification DROP accessKey');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'add access keys and emails dest to notifications';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ADD addressesEmails JSON NOT NULL DEFAULT \'[]\';');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ADD accessKey TEXT DEFAULT NULL');
|
||||||
|
$this->addSql('WITH randoms AS (select
|
||||||
|
n.id,
|
||||||
|
string_agg(substr(characters, (random() * length(characters) + 0.5)::integer, 1), \'\') as random_word
|
||||||
|
from (values(\'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\')) as symbols(characters)
|
||||||
|
-- length of word
|
||||||
|
join generate_series(1, 16) on 1 = 1
|
||||||
|
JOIN chill_main_notification n ON true
|
||||||
|
GROUP BY n.id)
|
||||||
|
UPDATE chill_main_notification SET accessKey = randoms.random_word FROM randoms WHERE chill_main_notification.id = randoms.id');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ALTER accessKey DROP DEFAULT');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ALTER accessKey SET NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20220413225830 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_address ALTER COLUMN postcode_id DROP NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Set postal code in chill_main_address NOT NULL';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_address ALTER COLUMN postcode_id SET NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20220413230159 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_address DROP CONSTRAINT chill_custom_validfrom_before_validto');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'in addresses, validfrom must be lower than validto';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_address ADD CONSTRAINT chill_custom_validfrom_before_validto CHECK (validfrom <= COALESCE(validto, \'infinity\'::date))');
|
||||||
|
}
|
||||||
|
}
|
@ -450,4 +450,10 @@ notification:
|
|||||||
subject: Objet
|
subject: Objet
|
||||||
see_comments_thread: Voir le fil de commentaires associé
|
see_comments_thread: Voir le fil de commentaires associé
|
||||||
object_prefix: "[CHILL] notification - "
|
object_prefix: "[CHILL] notification - "
|
||||||
|
dest by email: Lien d'accès par email
|
||||||
|
Any email: Aucun email
|
||||||
|
Add an email: Ajouter un email
|
||||||
|
dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Un compte utilisateur sera toujours nécessaire.
|
||||||
|
Remove an email: Supprimer l'adresse email
|
||||||
|
Email with access link: Adresse email ayant reçu un lien d'accès
|
||||||
|
|
||||||
|
@ -13,10 +13,10 @@ namespace Chill\PersonBundle\AccompanyingPeriod\Events;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\Address;
|
use Chill\MainBundle\Entity\Address;
|
||||||
use Chill\MainBundle\Entity\Notification;
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Notification\NotificationPersisterInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
|
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Templating\EngineInterface;
|
use Symfony\Component\Templating\EngineInterface;
|
||||||
@ -26,7 +26,7 @@ class PersonAddressMoveEventSubscriber implements EventSubscriberInterface
|
|||||||
{
|
{
|
||||||
private EngineInterface $engine;
|
private EngineInterface $engine;
|
||||||
|
|
||||||
private EntityManagerInterface $entityManager;
|
private NotificationPersisterInterface $notificationPersister;
|
||||||
|
|
||||||
private Security $security;
|
private Security $security;
|
||||||
|
|
||||||
@ -34,12 +34,12 @@ class PersonAddressMoveEventSubscriber implements EventSubscriberInterface
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
EngineInterface $engine,
|
EngineInterface $engine,
|
||||||
EntityManagerInterface $entityManager,
|
NotificationPersisterInterface $notificationPersister,
|
||||||
Security $security,
|
Security $security,
|
||||||
TranslatorInterface $translator
|
TranslatorInterface $translator
|
||||||
) {
|
) {
|
||||||
$this->engine = $engine;
|
$this->engine = $engine;
|
||||||
$this->entityManager = $entityManager;
|
$this->notificationPersister = $notificationPersister;
|
||||||
$this->security = $security;
|
$this->security = $security;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ class PersonAddressMoveEventSubscriber implements EventSubscriberInterface
|
|||||||
'period' => $period,
|
'period' => $period,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$this->entityManager->persist($notification);
|
$this->notificationPersister->persist($notification);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ namespace Chill\PersonBundle\AccompanyingPeriod\Events;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\Notification;
|
use Chill\MainBundle\Entity\Notification;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Notification\NotificationPersisterInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Doctrine\Persistence\Event\LifecycleEventArgs;
|
use Doctrine\Persistence\Event\LifecycleEventArgs;
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
@ -24,20 +24,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||||||
|
|
||||||
class UserRefEventSubscriber implements EventSubscriberInterface
|
class UserRefEventSubscriber implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $em;
|
|
||||||
|
|
||||||
private EngineInterface $engine;
|
private EngineInterface $engine;
|
||||||
|
|
||||||
|
private NotificationPersisterInterface $notificationPersister;
|
||||||
|
|
||||||
private Security $security;
|
private Security $security;
|
||||||
|
|
||||||
private TranslatorInterface $translator;
|
private TranslatorInterface $translator;
|
||||||
|
|
||||||
public function __construct(Security $security, TranslatorInterface $translator, EngineInterface $engine, EntityManagerInterface $em)
|
public function __construct(Security $security, TranslatorInterface $translator, EngineInterface $engine, NotificationPersisterInterface $notificationPersister)
|
||||||
{
|
{
|
||||||
$this->security = $security;
|
$this->security = $security;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
$this->engine = $engine;
|
$this->engine = $engine;
|
||||||
$this->em = $em;
|
$this->notificationPersister = $notificationPersister;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getSubscribedEvents()
|
public static function getSubscribedEvents()
|
||||||
@ -58,16 +58,13 @@ class UserRefEventSubscriber implements EventSubscriberInterface
|
|||||||
|
|
||||||
public function postUpdate(AccompanyingPeriod $period, LifecycleEventArgs $args): void
|
public function postUpdate(AccompanyingPeriod $period, LifecycleEventArgs $args): void
|
||||||
{
|
{
|
||||||
if ($period->hasPreviousUser()
|
if ($period->isChangedUser()
|
||||||
&& $period->getUser() !== $this->security->getUser()
|
&& $period->getUser() !== $this->security->getUser()
|
||||||
&& null !== $period->getUser()
|
&& null !== $period->getUser()
|
||||||
&& $period->getStep() !== AccompanyingPeriod::STEP_DRAFT
|
&& $period->getStep() !== AccompanyingPeriod::STEP_DRAFT
|
||||||
) {
|
) {
|
||||||
$this->generateNotificationToUser($period);
|
$this->generateNotificationToUser($period);
|
||||||
}
|
}
|
||||||
|
|
||||||
// we are just out of a flush operation. Launch a new one
|
|
||||||
$this->em->flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateNotificationToUser(AccompanyingPeriod $period)
|
private function generateNotificationToUser(AccompanyingPeriod $period)
|
||||||
@ -89,7 +86,7 @@ class UserRefEventSubscriber implements EventSubscriberInterface
|
|||||||
))
|
))
|
||||||
->addAddressee($period->getUser());
|
->addAddressee($period->getUser());
|
||||||
|
|
||||||
$this->em->persist($notification);
|
$this->notificationPersister->persist($notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function onPeriodConfirmed(AccompanyingPeriod $period)
|
private function onPeriodConfirmed(AccompanyingPeriod $period)
|
||||||
|
@ -102,7 +102,6 @@ class HouseholdApiController extends ApiController
|
|||||||
$event
|
$event
|
||||||
->setPreviousAddress($household->getPreviousAddressOf($address))
|
->setPreviousAddress($household->getPreviousAddressOf($address))
|
||||||
->setNextAddress($address);
|
->setNextAddress($address);
|
||||||
dump($event);
|
|
||||||
$this->eventDispatcher->dispatch($event);
|
$this->eventDispatcher->dispatch($event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,13 +12,16 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Controller;
|
namespace Chill\PersonBundle\Controller;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Address;
|
use Chill\MainBundle\Entity\Address;
|
||||||
|
use Chill\MainBundle\Form\Type\AddressDateType;
|
||||||
use Chill\PersonBundle\Entity\Household\Household;
|
use Chill\PersonBundle\Entity\Household\Household;
|
||||||
use Chill\PersonBundle\Form\HouseholdType;
|
use Chill\PersonBundle\Form\HouseholdType;
|
||||||
use Chill\PersonBundle\Repository\Household\PositionRepository;
|
use Chill\PersonBundle\Repository\Household\PositionRepository;
|
||||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||||
|
use Chill\PersonBundle\Security\Authorization\HouseholdVoter;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
@ -186,6 +189,62 @@ class HouseholdController extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(
|
||||||
|
* "/{household_id}/address/edit_valid_from",
|
||||||
|
* name="chill_person_household_address_valid_from_edit",
|
||||||
|
* methods={"GET", "HEAD", "POST"}
|
||||||
|
* )
|
||||||
|
* @ParamConverter("household", options={"id": "household_id"})
|
||||||
|
*/
|
||||||
|
public function addressValidFromEdit(Request $request, Household $household)
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted(HouseholdVoter::EDIT, $household);
|
||||||
|
|
||||||
|
if (!$request->query->has('address_id')) {
|
||||||
|
throw new BadRequestException('parameter address_id is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
$address_id = $request->query->getInt('address_id');
|
||||||
|
|
||||||
|
// loop over adresses of the household, to be sure that the household is associated
|
||||||
|
// to the edited address
|
||||||
|
$address = null;
|
||||||
|
|
||||||
|
foreach ($household->getAddresses() as $householdAddress) {
|
||||||
|
if ($householdAddress->getId() === $address_id) {
|
||||||
|
$address = $householdAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $address) {
|
||||||
|
throw new BadRequestException('The edited address does not belongs to the household');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(AddressDateType::class, $address, []);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$household->makeAddressConsistent();
|
||||||
|
|
||||||
|
$this->getDoctrine()->getManager()->flush();
|
||||||
|
|
||||||
|
return $this->redirectToRoute('chill_person_household_addresses', [
|
||||||
|
'household_id' => $household->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render(
|
||||||
|
'@ChillPerson/Household/address_valid_from_edit.html.twig',
|
||||||
|
[
|
||||||
|
'household' => $household,
|
||||||
|
'address' => $address,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(
|
* @Route(
|
||||||
* "/{household_id}/members/metadata/edit",
|
* "/{household_id}/members/metadata/edit",
|
||||||
|
@ -361,6 +361,8 @@ class AccompanyingPeriod implements
|
|||||||
*/
|
*/
|
||||||
private Collection $userHistories;
|
private Collection $userHistories;
|
||||||
|
|
||||||
|
private bool $userIsChanged = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary field, which is filled when the user is changed.
|
* Temporary field, which is filled when the user is changed.
|
||||||
*
|
*
|
||||||
@ -978,6 +980,11 @@ class AccompanyingPeriod implements
|
|||||||
return null !== $this->userPrevious;
|
return null !== $this->userPrevious;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isChangedUser(): bool
|
||||||
|
{
|
||||||
|
return $this->userIsChanged && $this->user !== $this->userPrevious;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the closing date is after the opening date.
|
* Returns true if the closing date is after the opening date.
|
||||||
*/
|
*/
|
||||||
@ -1103,6 +1110,14 @@ class AccompanyingPeriod implements
|
|||||||
$this->setStep(AccompanyingPeriod::STEP_CONFIRMED);
|
$this->setStep(AccompanyingPeriod::STEP_CONFIRMED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function resetPreviousUser(): self
|
||||||
|
{
|
||||||
|
$this->userPrevious = null;
|
||||||
|
$this->userIsChanged = false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Groups({"write"})
|
* @Groups({"write"})
|
||||||
*/
|
*/
|
||||||
@ -1329,6 +1344,7 @@ class AccompanyingPeriod implements
|
|||||||
{
|
{
|
||||||
if ($this->user !== $user) {
|
if ($this->user !== $user) {
|
||||||
$this->userPrevious = $this->user;
|
$this->userPrevious = $this->user;
|
||||||
|
$this->userIsChanged = true;
|
||||||
|
|
||||||
foreach ($this->userHistories as $history) {
|
foreach ($this->userHistories as $history) {
|
||||||
if (null === $history->getEndDate()) {
|
if (null === $history->getEndDate()) {
|
||||||
|
@ -57,6 +57,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
|
|||||||
* orphanRemoval=true
|
* orphanRemoval=true
|
||||||
* )
|
* )
|
||||||
* @Serializer\Groups({"read", "docgen:read"})
|
* @Serializer\Groups({"read", "docgen:read"})
|
||||||
|
* @ORM\OrderBy({"startDate": "DESC", "id": "DESC"})
|
||||||
*
|
*
|
||||||
* @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer`
|
* @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer`
|
||||||
*/
|
*/
|
||||||
|
@ -77,6 +77,7 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU
|
|||||||
* cascade={"remove", "persist"},
|
* cascade={"remove", "persist"},
|
||||||
* orphanRemoval=true
|
* orphanRemoval=true
|
||||||
* )
|
* )
|
||||||
|
* @ORM\OrderBy({"createdAt": "DESC", "id": "DESC"})
|
||||||
* @Serializer\Groups({"read"})
|
* @Serializer\Groups({"read"})
|
||||||
*/
|
*/
|
||||||
private Collection $documents;
|
private Collection $documents;
|
||||||
|
@ -20,6 +20,7 @@ use DateTimeInterface;
|
|||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Entity
|
* @ORM\Entity
|
||||||
@ -88,6 +89,7 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
|
|||||||
* @Serializer\Groups({"read"})
|
* @Serializer\Groups({"read"})
|
||||||
* @Serializer\Groups({"write"})
|
* @Serializer\Groups({"write"})
|
||||||
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
|
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
|
||||||
|
* @Assert\Valid
|
||||||
*/
|
*/
|
||||||
private ?StoredObject $storedObject = null;
|
private ?StoredObject $storedObject = null;
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ use DateTimeInterface;
|
|||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||||
use Symfony\Component\Serializer\Annotation\Groups;
|
use Symfony\Component\Serializer\Annotation\Groups;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Entity
|
* @ORM\Entity
|
||||||
@ -40,6 +41,8 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
/**
|
/**
|
||||||
* @ORM\Column(type="text")
|
* @ORM\Column(type="text")
|
||||||
* @Groups({"read", "write"})
|
* @Groups({"read", "write"})
|
||||||
|
* @Assert\NotBlank
|
||||||
|
* @Assert\NotNull
|
||||||
*/
|
*/
|
||||||
private $content;
|
private $content;
|
||||||
|
|
||||||
|
@ -151,6 +151,9 @@ class Household
|
|||||||
return $this->addresses;
|
return $this->addresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|Address[]
|
||||||
|
*/
|
||||||
public function getAddressesOrdered(): array
|
public function getAddressesOrdered(): array
|
||||||
{
|
{
|
||||||
$addresses = $this->getAddresses()->toArray();
|
$addresses = $this->getAddresses()->toArray();
|
||||||
|
@ -128,6 +128,44 @@ class SocialAction
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In a SocialIssues's collection, find the elements which are an ancestor of
|
||||||
|
* other elements.
|
||||||
|
*
|
||||||
|
* The difference of the given list (thus, the elements which are **not** kept
|
||||||
|
* in the returned collection) are the most-grand-child elements of the list.
|
||||||
|
*
|
||||||
|
* Removing those elements of the Collection (which is not done by this method)
|
||||||
|
* will ensure that only the most descendent elements are present in the collection,
|
||||||
|
* (any ancestor of another element are present).
|
||||||
|
*
|
||||||
|
* @param Collection|SocialAction[] $socialActions
|
||||||
|
*
|
||||||
|
* @return Collection|SocialAction[] a list with the elements of the given list which are parent of other elements in the given list
|
||||||
|
*/
|
||||||
|
public static function findAncestorSocialActions(Collection $socialActions): Collection
|
||||||
|
{
|
||||||
|
$ancestors = new ArrayCollection();
|
||||||
|
|
||||||
|
foreach ($socialActions as $candidateChild) {
|
||||||
|
if ($ancestors->contains($candidateChild)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($socialActions as $candidateParent) {
|
||||||
|
if ($ancestors->contains($candidateParent)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($candidateChild->isDescendantOf($candidateParent)) {
|
||||||
|
$ancestors->add($candidateParent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ancestors;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection|self[]
|
* @return Collection|self[]
|
||||||
*/
|
*/
|
||||||
@ -169,7 +207,7 @@ class SocialAction
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection|self[] All the descendants with the current entity (this)
|
* @return Collection|self[] All the descendants including the current entity (this)
|
||||||
*/
|
*/
|
||||||
public function getDescendantsWithThis(): Collection
|
public function getDescendantsWithThis(): Collection
|
||||||
{
|
{
|
||||||
@ -233,6 +271,23 @@ class SocialAction
|
|||||||
return $this->getParent() instanceof self;
|
return $this->getParent() instanceof self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursive method which return true if the current $action
|
||||||
|
* is a descendant of the $action given in parameter.
|
||||||
|
*/
|
||||||
|
public function isDescendantOf(SocialAction $action): bool
|
||||||
|
{
|
||||||
|
if (!$this->hasParent()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->getParent() === $action) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getParent()->isDescendantOf($action);
|
||||||
|
}
|
||||||
|
|
||||||
public function removeChild(self $child): self
|
public function removeChild(self $child): self
|
||||||
{
|
{
|
||||||
if ($this->children->removeElement($child)) {
|
if ($this->children->removeElement($child)) {
|
||||||
|
@ -202,7 +202,7 @@ class SocialIssue
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection|self[] All the descendants with the current entity (this)
|
* @return Collection|self[] All the descendants including the current entity (this)
|
||||||
*/
|
*/
|
||||||
public function getDescendantsWithThis(): Collection
|
public function getDescendantsWithThis(): Collection
|
||||||
{
|
{
|
||||||
|
@ -136,8 +136,11 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
|
|||||||
$andWhereSearchClause = [];
|
$andWhereSearchClause = [];
|
||||||
$andWhereSearchClauseArgs = [];
|
$andWhereSearchClauseArgs = [];
|
||||||
|
|
||||||
if ('' !== $default) {
|
if ('' !== trim($default)) {
|
||||||
foreach (explode(' ', $default) as $str) {
|
foreach (explode(' ', $default) as $str) {
|
||||||
|
if ('' === trim($str)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$pertinence[] =
|
$pertinence[] =
|
||||||
'STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ' .
|
'STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ' .
|
||||||
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + " .
|
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + " .
|
||||||
|
@ -30,6 +30,7 @@ div.list-with-period {
|
|||||||
|
|
||||||
// override wrap-list
|
// override wrap-list
|
||||||
div.wrap-list.periods-list {
|
div.wrap-list.periods-list {
|
||||||
|
padding-right: 1rem;
|
||||||
div.wl-row {
|
div.wl-row {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
div.wl-col {
|
div.wl-col {
|
||||||
@ -63,6 +64,7 @@ div.list-with-period {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.periods-list {
|
div.periods-list {
|
||||||
|
padding-right: 1rem;
|
||||||
div.title {
|
div.title {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
div.date {}
|
div.date {}
|
||||||
|
@ -5,9 +5,8 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
<label class="col-form-label col-sm-4">{{ $t('startdate.date') }}</label>
|
<div class="col-sm-12 date-update">
|
||||||
<div class="col-sm-8">
|
<input class="form-control" type="date" id="startDate" v-model="startDateInput">
|
||||||
<input class="form-control" type="date" v-model="startDate" @change="updateStartDate" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -17,30 +16,54 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { datetimeToISO, dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date.js';
|
import { dateToISO, ISOToDatetime} from 'ChillMainAssets/chill/js/date.js';
|
||||||
import { mapState, mapGetters } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'startDate',
|
name: 'startDate',
|
||||||
methods: {
|
data() {
|
||||||
updateStartDate(event) {
|
return {
|
||||||
const date = event.target.value;
|
lastRecordedDate: null
|
||||||
// console.log(date)
|
}
|
||||||
this.$store.dispatch('updateStartDate', date)
|
|
||||||
.catch(({name, violations}) => {
|
|
||||||
if (name === 'ValidationException' || name === 'AccessException') {
|
|
||||||
violations.forEach((violation) => this.$toast.open({message: violation}));
|
|
||||||
} else {
|
|
||||||
this.$toast.open({message: 'An error occurred'})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
startDate: state => dateToISO(ISOToDatetime(state.accompanyingCourse.openingDate.datetime))
|
startDate: state => dateToISO(ISOToDatetime(state.accompanyingCourse.openingDate.datetime))
|
||||||
})
|
}),
|
||||||
|
startDateInput: {
|
||||||
|
get() {
|
||||||
|
return this.startDate;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.lastRecordedDate = value;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('timeout finished')
|
||||||
|
if (this.lastRecordedDate === value) {
|
||||||
|
console.log('last recorded', this.lastRecordedDate, 'value', value)
|
||||||
|
this.$store.dispatch('updateStartDate', value)
|
||||||
|
.catch(({name, violations}) => {
|
||||||
|
if (name === 'ValidationException' || name === 'AccessException') {
|
||||||
|
violations.forEach((violation) => this.$toast.open({message: violation}));
|
||||||
|
} else {
|
||||||
|
this.$toast.open({message: 'An error occurred'})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.date-update {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
&-btn {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -59,8 +59,8 @@ const appMessages = {
|
|||||||
ok: "Oui, l'usager quitte le parcours",
|
ok: "Oui, l'usager quitte le parcours",
|
||||||
show_household_number: "Voir le ménage (n° {id})",
|
show_household_number: "Voir le ménage (n° {id})",
|
||||||
show_household: "Voir le ménage",
|
show_household: "Voir le ménage",
|
||||||
person_without_household_warning: "Certaines usagers n'appartiennent actuellement à aucun ménage. Renseignez leur appartenance dès que possible.",
|
person_without_household_warning: "Certaines usagers n'appartiennent actuellement à aucun ménage. Veuillez les associer à un ménage dès que possible.",
|
||||||
update_household: "Renseigner l'appartenance",
|
update_household: "Associer à un ménage",
|
||||||
participation_not_valid: "Sélectionnez ou créez au minimum 1 usager",
|
participation_not_valid: "Sélectionnez ou créez au minimum 1 usager",
|
||||||
},
|
},
|
||||||
requestor: {
|
requestor: {
|
||||||
@ -153,7 +153,7 @@ const appMessages = {
|
|||||||
},
|
},
|
||||||
startdate: {
|
startdate: {
|
||||||
change: "Date d'ouverture",
|
change: "Date d'ouverture",
|
||||||
date: "Date d'ouverture",
|
// update: "La nouvelle date d'ouverture a été enregistrée"
|
||||||
},
|
},
|
||||||
// catch errors
|
// catch errors
|
||||||
'Error while updating AccompanyingPeriod Course.': "Erreur du serveur lors de la mise à jour du parcours d'accompagnement.",
|
'Error while updating AccompanyingPeriod Course.': "Erreur du serveur lors de la mise à jour du parcours d'accompagnement.",
|
||||||
|
@ -322,7 +322,6 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateStartDate(state, date) {
|
updateStartDate(state, date) {
|
||||||
console.log('new state date', date)
|
|
||||||
state.accompanyingCourse.openingDate = date;
|
state.accompanyingCourse.openingDate = date;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -814,11 +813,9 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateStartDate({commit}, payload) {
|
updateStartDate({commit}, payload) {
|
||||||
console.log('payload', payload)
|
|
||||||
const date = ISOToDate(payload);
|
|
||||||
const url = `/api/1.0/person/accompanying-course/${id}.json`;
|
const url = `/api/1.0/person/accompanying-course/${id}.json`;
|
||||||
const body = { type: "accompanying_period", openingDate: { datetime: datetimeToISO(date) }};
|
const date = payload === null || payload === '' ? null : { datetime: datetimeToISO(ISOToDate(payload)) }
|
||||||
console.log('body', body)
|
const body = { type: "accompanying_period", openingDate: date};
|
||||||
return makeFetch('PATCH', url, body)
|
return makeFetch('PATCH', url, body)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
commit('updateStartDate', response.openingDate);
|
commit('updateStartDate', response.openingDate);
|
||||||
|
@ -227,19 +227,18 @@ export default {
|
|||||||
},
|
},
|
||||||
startDate: {
|
startDate: {
|
||||||
get() {
|
get() {
|
||||||
let d = this.$store.state.startDate;
|
return this.$store.state.startDate;
|
||||||
return dateToISO(d);
|
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
this.$store.commit('setStartDate', ISOToDate(value));
|
this.$store.commit('setStartDate', value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
endDate: {
|
endDate: {
|
||||||
get() {
|
get() {
|
||||||
return dateToISO(this.$store.state.endDate);
|
return this.$store.state.endDate;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
this.$store.commit('setEndDate', ISOToDate(value));
|
this.$store.commit('setEndDate', value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setSocialIssue: {
|
setSocialIssue: {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import { createStore } from 'vuex';
|
import { createStore } from 'vuex';
|
||||||
import { datetimeToISO } from 'ChillMainAssets/chill/js/date.js';
|
import { datetimeToISO, dateToISO, ISOToDate, ISOToDatetime } from 'ChillMainAssets/chill/js/date.js';
|
||||||
import { findSocialActionsBySocialIssue } from 'ChillPersonAssets/vuejs/_api/SocialWorkSocialAction.js';
|
import { findSocialActionsBySocialIssue } from 'ChillPersonAssets/vuejs/_api/SocialWorkSocialAction.js';
|
||||||
// import { create } from 'ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js';
|
// import { create } from 'ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js';
|
||||||
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
|
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
|
||||||
@ -20,7 +20,7 @@ const store = createStore({
|
|||||||
.map(p => p.person),
|
.map(p => p.person),
|
||||||
personsReachables: window.accompanyingCourse.participations.filter(p => p.endDate == null)
|
personsReachables: window.accompanyingCourse.participations.filter(p => p.endDate == null)
|
||||||
.map(p => p.person),
|
.map(p => p.person),
|
||||||
startDate: new Date(),
|
startDate: dateToISO(new Date()),
|
||||||
endDate: null,
|
endDate: null,
|
||||||
isLoadingSocialActions: false,
|
isLoadingSocialActions: false,
|
||||||
isPostingWork: false,
|
isPostingWork: false,
|
||||||
@ -47,7 +47,7 @@ const store = createStore({
|
|||||||
id: state.socialActionPicked.id
|
id: state.socialActionPicked.id
|
||||||
},
|
},
|
||||||
startDate: {
|
startDate: {
|
||||||
datetime: datetimeToISO(state.startDate)
|
datetime: datetimeToISO(ISOToDate(state.startDate))
|
||||||
},
|
},
|
||||||
persons: []
|
persons: []
|
||||||
};
|
};
|
||||||
@ -61,7 +61,7 @@ const store = createStore({
|
|||||||
|
|
||||||
if (null !== state.endDate) {
|
if (null !== state.endDate) {
|
||||||
payload.endDate = {
|
payload.endDate = {
|
||||||
datetime: datetimeToISO(state.endDate)
|
datetime: datetimeToISO(ISOToDate(state.endDate))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +111,7 @@ const store = createStore({
|
|||||||
state.startDate = date;
|
state.startDate = date;
|
||||||
},
|
},
|
||||||
setEndDate(state, date) {
|
setEndDate(state, date) {
|
||||||
|
console.log(date)
|
||||||
state.endDate = date;
|
state.endDate = date;
|
||||||
},
|
},
|
||||||
setPersonsPickedIds(state, ids) {
|
setPersonsPickedIds(state, ids) {
|
||||||
|
@ -439,18 +439,18 @@ export default {
|
|||||||
]),
|
]),
|
||||||
startDate: {
|
startDate: {
|
||||||
get() {
|
get() {
|
||||||
return dateToISO(this.$store.state.startDate);
|
return this.$store.state.startDate;
|
||||||
},
|
},
|
||||||
set(v) {
|
set(v) {
|
||||||
this.$store.commit('setStartDate', ISOToDate(v));
|
this.$store.commit('setStartDate', v);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
endDate: {
|
endDate: {
|
||||||
get() {
|
get() {
|
||||||
return dateToISO(this.$store.state.endDate);
|
return this.$store.state.endDate;
|
||||||
},
|
},
|
||||||
set(v) {
|
set(v) {
|
||||||
this.$store.commit('setEndDate', ISOToDate(v));
|
this.$store.commit('setEndDate', v);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
note: {
|
note: {
|
||||||
@ -521,12 +521,12 @@ export default {
|
|||||||
this.$store.commit('removeReferrer', u);
|
this.$store.commit('removeReferrer', u);
|
||||||
},
|
},
|
||||||
goToGenerateWorkflow({link}) {
|
goToGenerateWorkflow({link}) {
|
||||||
console.log('save before leave to generate workflow')
|
// console.log('save before leave to generate workflow')
|
||||||
const callback = (data) => {
|
const callback = (data) => {
|
||||||
window.location.assign(link);
|
window.location.assign(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.$store.dispatch('submit', callback)
|
return this.$store.dispatch('submit', callback)
|
||||||
.catch(e => { console.log(e); throw e; });
|
.catch(e => { console.log(e); throw e; });
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
|
@ -67,14 +67,17 @@
|
|||||||
<div class="flex-table">
|
<div class="flex-table">
|
||||||
<div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.id">
|
<div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.id">
|
||||||
<div class="item-row">
|
<div class="item-row">
|
||||||
<div class="input-group input-group-lg mb-3">
|
<div class="input-group input-group-lg mb-3 row">
|
||||||
<input
|
<label class="col-sm-3 col-form-label">Titre du document:</label>
|
||||||
class="form-control document-title"
|
<div class="col-sm-9">
|
||||||
type="text"
|
<input
|
||||||
:value="d.title"
|
class="form-control document-title"
|
||||||
:id="d.id"
|
type="text"
|
||||||
:data-key="i"
|
:value="d.title"
|
||||||
@input="onInputDocumentTitle"/>
|
:id="d.id"
|
||||||
|
:data-key="i"
|
||||||
|
@input="onInputDocumentTitle"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-row">
|
<div class="item-row">
|
||||||
@ -295,38 +298,39 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([
|
...mapState([
|
||||||
'isPosting'
|
'isPosting'
|
||||||
]),
|
]),
|
||||||
getTemplatesAvailables() {
|
getTemplatesAvailables() {
|
||||||
return this.$store.getters.getTemplatesAvailablesForEvaluation(this.evaluation.evaluation);
|
return this.$store.getters.getTemplatesAvailablesForEvaluation(this.evaluation.evaluation);
|
||||||
},
|
},
|
||||||
canGenerate() {
|
canGenerate() {
|
||||||
return !this.$store.state.isPosting && this.template !== null;
|
return !this.$store.state.isPosting && this.template !== null;
|
||||||
},
|
},
|
||||||
startDate: {
|
startDate: {
|
||||||
get() {
|
get() {
|
||||||
return dateToISO(this.evaluation.startDate);
|
console.log('evaluation', this.evaluation);
|
||||||
},
|
return this.evaluation.startDate;
|
||||||
set(v) {
|
},
|
||||||
this.$store.commit('setEvaluationStartDate', { key: this.evaluation.key, date: ISOToDate(v) });
|
set(v) {
|
||||||
}
|
this.$store.commit('setEvaluationStartDate', { key: this.evaluation.key, date: v });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
endDate: {
|
endDate: {
|
||||||
get() {
|
get() {
|
||||||
return dateToISO(this.evaluation.endDate);
|
return this.evaluation.endDate;
|
||||||
},
|
},
|
||||||
set(v) {
|
set(v) {
|
||||||
this.$store.commit('setEvaluationEndDate', { key: this.evaluation.key, date: ISOToDate(v) });
|
this.$store.commit('setEvaluationEndDate', { key: this.evaluation.key, date: v });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maxDate: {
|
maxDate: {
|
||||||
get() {
|
get() {
|
||||||
return dateToISO(this.evaluation.maxDate);
|
return this.evaluation.maxDate;
|
||||||
},
|
},
|
||||||
set(v) {
|
set(v) {
|
||||||
this.$store.commit('setEvaluationMaxDate', { key: this.evaluation.key, date: ISOToDate(v) });
|
this.$store.commit('setEvaluationMaxDate', { key: this.evaluation.key, date: v });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
warningInterval: {
|
warningInterval: {
|
||||||
get() { return this.evaluation.warningInterval; },
|
get() { return this.evaluation.warningInterval; },
|
||||||
@ -341,7 +345,7 @@ export default {
|
|||||||
ISOToDatetime,
|
ISOToDatetime,
|
||||||
canEditDocument(document) {
|
canEditDocument(document) {
|
||||||
return 'storedObject' in document ?
|
return 'storedObject' in document ?
|
||||||
this.mime.includes(document.storedObject.type) && document.storedObject.keyInfos.length === 0 : false;
|
this.mime.includes(document.storedObject.type) : false;
|
||||||
},
|
},
|
||||||
listAllStatus() {
|
listAllStatus() {
|
||||||
console.log('load all status');
|
console.log('load all status');
|
||||||
@ -357,16 +361,16 @@ export default {
|
|||||||
},
|
},
|
||||||
buildEditLink(storedObject) {
|
buildEditLink(storedObject) {
|
||||||
return `/wopi/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent(
|
return `/wopi/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent(
|
||||||
window.location.pathname + window.location.search + window.location.hash);
|
window.location.pathname + window.location.search + window.location.hash);
|
||||||
},
|
},
|
||||||
submitBeforeGenerate({template}) {
|
submitBeforeGenerate({template}) {
|
||||||
const callback = (data) => {
|
const callback = (data) => {
|
||||||
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id;
|
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id;
|
||||||
|
|
||||||
window.location.assign(buildLink(template, evaluationId, 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation'));
|
window.location.assign(buildLink(template, evaluationId, 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation'));
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.$store.dispatch('submit', callback).catch(e => { console.log(e); throw e; });
|
return this.$store.dispatch('submit', callback).catch(e => { console.log(e); throw e; });
|
||||||
},
|
},
|
||||||
onInputDocumentTitle(event) {
|
onInputDocumentTitle(event) {
|
||||||
const id = Number(event.target.id);
|
const id = Number(event.target.id);
|
||||||
@ -392,20 +396,20 @@ export default {
|
|||||||
},
|
},
|
||||||
removeDocument(document) {
|
removeDocument(document) {
|
||||||
if (window.confirm("Êtes-vous sûr·e de vouloir supprimer le document qui a pour titre \"" + document.title +"\" ?")) {
|
if (window.confirm("Êtes-vous sûr·e de vouloir supprimer le document qui a pour titre \"" + document.title +"\" ?")) {
|
||||||
this.$store.commit('removeDocument', {key: this.evaluation.key, document: document});
|
this.$store.commit('removeDocument', {key: this.evaluation.key, document: document});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goToGenerateWorkflowEvaluationDocument({event, link, workflowName, payload}) {
|
goToGenerateWorkflowEvaluationDocument({event, link, workflowName, payload}) {
|
||||||
const callback = (data) => {
|
const callback = (data) => {
|
||||||
let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key);
|
let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key);
|
||||||
let updatedDocument = evaluation.documents.find(d => d.key === payload.doc.key);
|
let updatedDocument = evaluation.documents.find(d => d.key === payload.doc.key);
|
||||||
window.location.assign(buildLinkCreate(workflowName,
|
window.location.assign(buildLinkCreate(workflowName,
|
||||||
'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', updatedDocument.id));
|
'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', updatedDocument.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.$store.dispatch('submit', callback)
|
return this.$store.dispatch('submit', callback)
|
||||||
.catch(e => { console.log(e); throw e; });
|
.catch(e => { console.log(e); throw e; });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createStore } from 'vuex';
|
import { createStore } from 'vuex';
|
||||||
import { datetimeToISO, ISOToDatetime, intervalDaysToISO, intervalISOToDays } from 'ChillMainAssets/chill/js/date.js';
|
import { dateToISO, ISOToDate, datetimeToISO, ISOToDatetime, intervalDaysToISO, intervalISOToDays } from 'ChillMainAssets/chill/js/date.js';
|
||||||
import { findSocialActionsBySocialIssue } from 'ChillPersonAssets/vuejs/_api/SocialWorkSocialAction.js';
|
import { findSocialActionsBySocialIssue } from 'ChillPersonAssets/vuejs/_api/SocialWorkSocialAction.js';
|
||||||
import { create } from 'ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js';
|
import { create } from 'ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js';
|
||||||
import { fetchResults, makeFetch } from 'ChillMainAssets/lib/api/apiMethods.js';
|
import { fetchResults, makeFetch } from 'ChillMainAssets/lib/api/apiMethods.js';
|
||||||
@ -13,9 +13,9 @@ const store = createStore({
|
|||||||
state: {
|
state: {
|
||||||
work: window.accompanyingCourseWork,
|
work: window.accompanyingCourseWork,
|
||||||
startDate: window.accompanyingCourseWork.startDate !== null ?
|
startDate: window.accompanyingCourseWork.startDate !== null ?
|
||||||
ISOToDatetime(window.accompanyingCourseWork.startDate.datetime) : null,
|
dateToISO(new Date(window.accompanyingCourseWork.startDate.datetime)) : null,
|
||||||
endDate: window.accompanyingCourseWork.endDate !== null ?
|
endDate: window.accompanyingCourseWork.endDate !== null ?
|
||||||
ISOToDatetime(window.accompanyingCourseWork.endDate.datetime) : null,
|
dateToISO(new Date(window.accompanyingCourseWork.endDate.datetime)) : null,
|
||||||
note: window.accompanyingCourseWork.note,
|
note: window.accompanyingCourseWork.note,
|
||||||
goalsPicked: window.accompanyingCourseWork.goals,
|
goalsPicked: window.accompanyingCourseWork.goals,
|
||||||
goalsForAction: [],
|
goalsForAction: [],
|
||||||
@ -72,11 +72,11 @@ const store = createStore({
|
|||||||
return {
|
return {
|
||||||
type: 'accompanying_period_work',
|
type: 'accompanying_period_work',
|
||||||
id: state.work.id,
|
id: state.work.id,
|
||||||
startDate: state.startDate === null ? null : {
|
startDate: state.startDate === null || state.startDate === '' ? null : {
|
||||||
datetime: datetimeToISO(state.startDate)
|
datetime: datetimeToISO(ISOToDate(state.startDate))
|
||||||
},
|
},
|
||||||
endDate: state.endDate === null ? null : {
|
endDate: state.endDate === null || state.endDate === '' ? null : {
|
||||||
datetime: datetimeToISO(state.endDate)
|
datetime: datetimeToISO(ISOToDate(state.endDate))
|
||||||
},
|
},
|
||||||
note: state.note,
|
note: state.note,
|
||||||
persons: state.personsPicked.map(p => ({id: p.id, type: p.type})),
|
persons: state.personsPicked.map(p => ({id: p.id, type: p.type})),
|
||||||
@ -110,9 +110,9 @@ const store = createStore({
|
|||||||
id: e.evaluation.id,
|
id: e.evaluation.id,
|
||||||
type: e.evaluation.type
|
type: e.evaluation.type
|
||||||
},
|
},
|
||||||
startDate: e.startDate !== null ? { datetime: datetimeToISO(e.startDate) } : null,
|
startDate: e.startDate === null || e.startDate === '' ? null : { datetime: datetimeToISO(ISOToDate(e.startDate)) },
|
||||||
endDate: e.endDate !== null ? { datetime: datetimeToISO(e.endDate) } : null,
|
endDate: e.endDate === null || e.endDate === '' ? null : { datetime: datetimeToISO(ISOToDate(e.endDate)) },
|
||||||
maxDate: e.maxDate !== null ? { datetime: datetimeToISO(e.maxDate) } : null,
|
maxDate: e.maxDate === null || e.maxDate === '' ? null : { datetime: datetimeToISO(ISOToDate(e.maxDate)) },
|
||||||
warningInterval: intervalDaysToISO(e.warningInterval),
|
warningInterval: intervalDaysToISO(e.warningInterval),
|
||||||
comment: e.comment,
|
comment: e.comment,
|
||||||
documents: e.documents
|
documents: e.documents
|
||||||
@ -132,9 +132,9 @@ const store = createStore({
|
|||||||
var k = Object.assign(e, {
|
var k = Object.assign(e, {
|
||||||
key: index,
|
key: index,
|
||||||
editEvaluation: false,
|
editEvaluation: false,
|
||||||
startDate: e.startDate !== null ? ISOToDatetime(e.startDate.datetime) : null,
|
startDate: e.startDate !== null ? dateToISO(new Date(e.startDate.datetime)) : null,
|
||||||
endDate: e.endDate !== null ? ISOToDatetime(e.endDate.datetime) : null,
|
endDate: e.endDate !== null ? dateToISO(new Date(e.endDate.datetime)) : null,
|
||||||
maxDate: e.maxDate !== null ? ISOToDatetime(e.maxDate.datetime) : null,
|
maxDate: e.maxDate !== null ? dateToISO(new Date(e.maxDate.datetime)) : null,
|
||||||
warningInterval: e.warningInterval !== null ? intervalISOToDays(e.warningInterval) : null,
|
warningInterval: e.warningInterval !== null ? intervalISOToDays(e.warningInterval) : null,
|
||||||
documents: e.documents.map((d, docIndex) => {
|
documents: e.documents.map((d, docIndex) => {
|
||||||
return Object.assign(d, {
|
return Object.assign(d, {
|
||||||
@ -244,7 +244,7 @@ const store = createStore({
|
|||||||
type: "accompanying_period_work_evaluation",
|
type: "accompanying_period_work_evaluation",
|
||||||
key: state.evaluationsPicked.length + 1,
|
key: state.evaluationsPicked.length + 1,
|
||||||
evaluation: evaluation,
|
evaluation: evaluation,
|
||||||
startDate: new Date(),
|
startDate: dateToISO(new Date()),
|
||||||
endDate: null,
|
endDate: null,
|
||||||
maxDate: null,
|
maxDate: null,
|
||||||
warningInterval: null,
|
warningInterval: null,
|
||||||
@ -264,6 +264,7 @@ const store = createStore({
|
|||||||
.startDate = date;
|
.startDate = date;
|
||||||
},
|
},
|
||||||
setEvaluationEndDate(state, {key, date}) {
|
setEvaluationEndDate(state, {key, date}) {
|
||||||
|
console.log('commit date', date)
|
||||||
state.evaluationsPicked.find(e => e.key === key)
|
state.evaluationsPicked.find(e => e.key === key)
|
||||||
.endDate = date;
|
.endDate = date;
|
||||||
},
|
},
|
||||||
|
@ -53,7 +53,7 @@ export default {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
this.$store.commit('setHouseholdCompositionType', value);
|
this.$store.dispatch('setHouseholdCompositionType', value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
numberOfChildren: {
|
numberOfChildren: {
|
||||||
@ -66,18 +66,19 @@ export default {
|
|||||||
},
|
},
|
||||||
startDate: {
|
startDate: {
|
||||||
get() {
|
get() {
|
||||||
return [
|
return this.$store.state.startDate;
|
||||||
this.$store.state.startDate.getFullYear(),
|
// return [
|
||||||
(this.$store.state.startDate.getMonth() + 1).toString().padStart(2, '0'),
|
// this.$store.state.startDate.getFullYear(),
|
||||||
this.$store.state.startDate.getDate().toString().padStart(2, '0')
|
// (this.$store.state.startDate.getMonth() + 1).toString().padStart(2, '0'),
|
||||||
].join('-');
|
// this.$store.state.startDate.getDate().toString().padStart(2, '0')
|
||||||
|
// ].join('-');
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
let
|
// let
|
||||||
[year, month, day] = value.split('-'),
|
// [year, month, day] = value.split('-'),
|
||||||
dValue = new Date(year, month-1, day);
|
// dValue = new Date(year, month-1, day);
|
||||||
|
|
||||||
this.$store.dispatch('setStartDate', dValue);
|
this.$store.dispatch('setStartDate', value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { createStore } from 'vuex';
|
|||||||
import { householdMove, fetchHouseholdSuggestionByAccompanyingPeriod, fetchAddressSuggestionByPerson} from './../api.js';
|
import { householdMove, fetchHouseholdSuggestionByAccompanyingPeriod, fetchAddressSuggestionByPerson} from './../api.js';
|
||||||
import { fetchResults } from 'ChillMainAssets/lib/api/apiMethods.js'
|
import { fetchResults } from 'ChillMainAssets/lib/api/apiMethods.js'
|
||||||
import { fetchHouseholdByAddressReference } from 'ChillPersonAssets/lib/household.js';
|
import { fetchHouseholdByAddressReference } from 'ChillPersonAssets/lib/household.js';
|
||||||
import { datetimeToISO } from 'ChillMainAssets/chill/js/date.js';
|
import { datetimeToISO, dateToISO, ISOToDate } from 'ChillMainAssets/chill/js/date.js';
|
||||||
|
|
||||||
const debug = process.env.NODE_ENV !== 'production';
|
const debug = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ const store = createStore({
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}),
|
}),
|
||||||
startDate: new Date(),
|
startDate: dateToISO(new Date()),
|
||||||
/**
|
/**
|
||||||
* Indicates if the destination is:
|
* Indicates if the destination is:
|
||||||
*
|
*
|
||||||
@ -278,7 +278,7 @@ const store = createStore({
|
|||||||
type: conc.person.type
|
type: conc.person.type
|
||||||
},
|
},
|
||||||
start_date: {
|
start_date: {
|
||||||
datetime: datetimeToISO(state.startDate)
|
datetime: state.startDate === null || state.startDate === '' ? null : datetimeToISO(ISOToDate(state.startDate))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -302,7 +302,7 @@ const store = createStore({
|
|||||||
},
|
},
|
||||||
number_of_children: state.numberOfChildren,
|
number_of_children: state.numberOfChildren,
|
||||||
start_date: {
|
start_date: {
|
||||||
datetime: datetimeToISO(state.startDate),
|
datetime: datetimeToISO(ISOToDate(state.startDate)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -430,7 +430,7 @@ const store = createStore({
|
|||||||
state.householdCompositionTypes = types;
|
state.householdCompositionTypes = types;
|
||||||
},
|
},
|
||||||
setHouseholdCompositionType(state, id) {
|
setHouseholdCompositionType(state, id) {
|
||||||
state.householdCompositionType = state.householdCompositionTypes.find(t => t.id = id);
|
state.householdCompositionType = state.householdCompositionTypes.find(t => t.id === id);
|
||||||
},
|
},
|
||||||
setNumberOfChildren(state, number) {
|
setNumberOfChildren(state, number) {
|
||||||
state.numberOfChildren = Number.parseInt(number);
|
state.numberOfChildren = Number.parseInt(number);
|
||||||
@ -495,6 +495,12 @@ const store = createStore({
|
|||||||
setComment({ commit }, payload) {
|
setComment({ commit }, payload) {
|
||||||
commit('setComment', payload);
|
commit('setComment', payload);
|
||||||
},
|
},
|
||||||
|
setHouseholdCompositionTypes({ commit }, payload) {
|
||||||
|
commit('setHouseholdCompositionTypes', payload);
|
||||||
|
},
|
||||||
|
setHouseholdCompositionType({ commit }, payload) {
|
||||||
|
commit('setHouseholdCompositionType', payload);
|
||||||
|
},
|
||||||
fetchHouseholdSuggestionForConcerned({ commit, state }, person) {
|
fetchHouseholdSuggestionForConcerned({ commit, state }, person) {
|
||||||
fetchHouseholdSuggestionByAccompanyingPeriod(person.id)
|
fetchHouseholdSuggestionByAccompanyingPeriod(person.id)
|
||||||
.then(households => {
|
.then(households => {
|
||||||
@ -597,7 +603,7 @@ if (concerned.length > 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchResults(`/api/1.0/person/houehold/composition/type.json`).then(types => {
|
fetchResults(`/api/1.0/person/houehold/composition/type.json`).then(types => {
|
||||||
store.commit('setHouseholdCompositionTypes', types);
|
store.dispatch('setHouseholdCompositionTypes', types);
|
||||||
})
|
})
|
||||||
|
|
||||||
export { store };
|
export { store };
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{{ form_start(form) }}
|
{{ form_start(form) }}
|
||||||
{{ form_errors(form) }}
|
{{ form_errors(form) }}
|
||||||
|
{{ form_errors(form.content) }}
|
||||||
{{ form_widget(form.content) }}
|
{{ form_widget(form.content) }}
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
<li>
|
<li>
|
||||||
|
@ -25,22 +25,6 @@
|
|||||||
<div class="item-row separator">
|
<div class="item-row separator">
|
||||||
<div class="wrap-list">
|
<div class="wrap-list">
|
||||||
|
|
||||||
{% if w.createdBy %}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title">
|
|
||||||
<h3>{{ 'Referrers'|trans }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
<p class="wl-item">
|
|
||||||
{% for u in w.referrers %}
|
|
||||||
{{ u|chill_entity_render_box }}
|
|
||||||
{% if not loop.last %}, {% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{%- if w.persons -%}
|
{%- if w.persons -%}
|
||||||
<div class="wl-row">
|
<div class="wl-row">
|
||||||
<div class="wl-col title">
|
<div class="wl-col title">
|
||||||
@ -78,6 +62,22 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{%- if w.referrers -%}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">
|
||||||
|
<h3>{{ 'Referrers'|trans }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
{% for u in w.referrers %}
|
||||||
|
<span class="wl-item">
|
||||||
|
{{ u|chill_entity_render_box }}
|
||||||
|
{% if not loop.last %}, {% endif %}
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{%- if w.socialAction.issue -%}
|
{%- if w.socialAction.issue -%}
|
||||||
<div class="wl-row">
|
<div class="wl-row">
|
||||||
<div class="wl-col title">
|
<div class="wl-col title">
|
||||||
|
@ -28,6 +28,15 @@
|
|||||||
<span class="badge-thirdparty">{{ w.handlingThierParty|chill_entity_render_box }}</span>
|
<span class="badge-thirdparty">{{ w.handlingThierParty|chill_entity_render_box }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if w.referrers %}
|
||||||
|
<li>
|
||||||
|
<span class="item-key">{{ 'Referrers'|trans ~ ' : ' }}</span>
|
||||||
|
{% for u in w.referrers %}
|
||||||
|
<span class="badge-user">{{ u|chill_entity_render_box }}</span>
|
||||||
|
{% if not loop.last %}, {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
<li class="associated-persons">
|
<li class="associated-persons">
|
||||||
<span class="item-key">{{ 'Participants'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'Participants'|trans ~ ' : ' }}</span>
|
||||||
{% for p in w.persons %}
|
{% for p in w.persons %}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
{% elseif period.step == 'CONFIRMED' %}
|
{% elseif period.step == 'CONFIRMED' %}
|
||||||
<span class="badge bg-primary">{{- 'Confirmed'|trans|upper -}}</span>
|
<span class="badge bg-primary">{{- 'Confirmed'|trans|upper -}}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-primary">{{- 'Closed'|trans|upper -}}</span>
|
<span class="badge bg-danger">{{- 'Closed'|trans|upper -}}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -116,14 +116,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="item-row separator">
|
|
||||||
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', period.id) %}
|
{% if show_pinned_comment|default(false) and period.pinnedComment is not empty%}
|
||||||
{% if notif_counter.total > 0 %}
|
<div class="item-row separator comment">
|
||||||
<div class="item-col item-meta">
|
<div style="width: 100%">
|
||||||
{{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', period.id) }}
|
<h4 class="item-key visually-hidden">{{ 'Pinned comment'|trans }}</h4>
|
||||||
|
<blockquote class="chill-user-quote">
|
||||||
|
{{ period.pinnedComment.content|u.truncate(750, '…', false)|chill_markdown_to_html }}
|
||||||
|
{% if period.pinnedComment.content|length > 750 %}
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_person_accompanying_period_comment_list', {'accompanying_period_id': period.id}) }}">{{ 'Read more'|trans }}</a>
|
||||||
|
{% endif %}
|
||||||
|
<div class="metadata">
|
||||||
|
{{ 'Last updated by'| trans }}
|
||||||
|
<span class="user">
|
||||||
|
{{ period.pinnedComment.updatedBy|chill_entity_render_box }}
|
||||||
|
</span>
|
||||||
|
{{ 'on'|trans ~ ' ' }}
|
||||||
|
<span class="date">
|
||||||
|
{{ period.pinnedComment.updatedAt|format_datetime("medium", "short") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
{% if itemMeta is defined %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="item-row separator">
|
||||||
|
{% if itemMeta is not defined %}
|
||||||
|
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', period.id) %}
|
||||||
|
{% if notif_counter.total > 0 %}
|
||||||
|
<div class="item-col item-meta">
|
||||||
|
{{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', period.id) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
{{ itemMeta }}
|
{{ itemMeta }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="item-col">
|
<div class="item-col">
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<h1>{{ 'My accompanying periods'|trans }}</h1>
|
<h1>{{ 'My accompanying periods'|trans }}</h1>
|
||||||
|
|
||||||
|
<p>{{ 'Number of periods'|trans }}: <span class="badge rounded-pill bg-primary">{{ pagination.totalItems }}</span></p>
|
||||||
|
|
||||||
<div class="flex-table accompanyingcourse-list">
|
<div class="flex-table accompanyingcourse-list">
|
||||||
{% for period in accompanyingPeriods %}
|
{% for period in accompanyingPeriods %}
|
||||||
{% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period, 'recordAction': _self.recordAction(period)} %}
|
{% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period, 'recordAction': _self.recordAction(period)} %}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user