Compare commits

..

6 Commits

318 changed files with 2245 additions and 11418 deletions

2
.gitignore vendored
View File

@@ -24,5 +24,3 @@ docs/build/
/.php-cs-fixer.cache
/.idea/
/.psalm/
node_modules/*

View File

@@ -9,22 +9,18 @@
],
"require": {
"php": "^7.4",
"ext-json": "*",
"ext-openssl": "*",
"ext-redis": "*",
"champs-libres/async-uploader-bundle": "dev-sf4#d57134aee8e504a83c902ff0cf9f8d36ac418290",
"champs-libres/wopi-bundle": "dev-master@dev",
"champs-libres/wopi-lib": "dev-master@dev",
"champs-libres/wopi-bundle": "dev-master#6dd8e0a14e00131eb4b889ecc30270ee4a0e5224",
"champs-libres/wopi-lib": "dev-master#8615f4a45a39fc2b6a98765ea835fcfd39618787",
"doctrine/doctrine-bundle": "^2.1",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.13.0",
"doctrine/orm": "^2.7",
"erusev/parsedown": "^1.7",
"graylog2/gelf-php": "^1.5",
"knplabs/knp-menu-bundle": "^3.0",
"knplabs/knp-time-bundle": "^1.12",
"knpuniversity/oauth2-client-bundle": "^2.10",
"league/csv": "^9.7.1",
"lexik/jwt-authentication-bundle": "^2.16",
"nyholm/psr7": "^1.4",
"ocramius/package-versions": "^1.10 || ^2",
"odolbeau/phone-number-bundle": "^3.6",
@@ -33,12 +29,12 @@
"ramsey/uuid-doctrine": "^1.7",
"sensio/framework-extra-bundle": "^5.5",
"spomky-labs/base64url": "^2.0",
"symfony/asset": "^4.4",
"symfony/browser-kit": "^4.4",
"symfony/css-selector": "^4.4",
"symfony/expression-language": "^4.4",
"symfony/form": "^4.4",
"symfony/framework-bundle": "^4.4",
"symfony/http-client": "^4.4 || ^5",
"symfony/http-foundation": "^4.4",
"symfony/intl": "^4.4",
"symfony/mailer": "^5.4",
@@ -76,7 +72,8 @@
"symfony/maker-bundle": "^1.20",
"symfony/phpunit-bridge": "^4.4",
"symfony/stopwatch": "^4.4",
"symfony/var-dumper": "^4.4"
"symfony/var-dumper": "^4.4",
"symfony/web-profiler-bundle": "^4.4"
},
"conflict": {
"symfony/symfony": "*"

View File

@@ -1,93 +0,0 @@
.. Copyright (C) 2014-2023 Champs Libres Cooperative SCRLFS
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
.. _cronjob:
Cron jobs
*********
Some tasks must be executed regularly: refresh some materialized views, remove old data, ...
For this purpose, one can programmatically implements a "cron job", which will be scheduled by a specific command.
The command :code:`chill:cron-job:execute`
==========================================
The command :code:`chill:cron-job:execute` will schedule a task, one by one. In a classical implementation, it should
be executed every 15 minutes (more or less), to ensure that every task can be executed.
.. warning::
This command should not be executed in parallel. The installer should ensure that two job are executed concurrently.
How to implements a cron job ?
==============================
Implements a :code:`Chill\MainBundle\Cron\CronJobInterface`. Here is an example:
.. code-block:: php
namespace Chill\MainBundle\Service\Something;
use Chill\MainBundle\Cron\CronJobInterface;
use Chill\MainBundle\Entity\CronJobExecution;
use DateInterval;
use DateTimeImmutable;
class MyCronJob implements CronJobInterface
{
public function canRun(?CronJobExecution $cronJobExecution): bool
{
// the parameter $cronJobExecution contains data about the last execution of the cronjob
// if it is null, it should be executed immediatly
if (null === $cronJobExecution) {
return true;
}
if ($cronJobExecution->getKey() !== $this->getKey()) {
throw new UnexpectedValueException();
}
// this cron job should be executed if the last execution is greater than one day, but only during the night
$now = new DateTimeImmutable('now');
return $cronJobExecution->getLastStart() < $now->sub(new DateInterval('P1D'))
&& in_array($now->format('H'), self::ACCEPTED_HOURS, true)
// introduce a random component to ensure a roll of task execution when multiple instances are hosted on same machines
&& mt_rand(0, 5) === 0;
}
public function getKey(): string
{
return 'arbitrary-and-unique-key';
}
public function run(): void
{
// here, we execute the command
}
}
How are cron job scheduled ?
============================
If the command :code:`chill:cron-job:execute` is run with one or more :code:`job` argument, those jobs are run, **without checking that the job can run** (the method :code:`canRun` is not executed).
If any :code:`job` argument is given, the :code:`CronManager` schedule job with those steps:
* the tasks are ordered, with:
* a priority is given for tasks that weren't never executed;
* then, the tasks are ordered, the last executed are the first in the list
* then, for each tasks, and in the given order, the first task where :code:`canRun` return :code:`TRUE` will be executed.
The command :code:`chill:cron-job:execute` execute **only one** task.

View File

@@ -34,7 +34,6 @@ As Chill rely on the `symfony <http://symfony.com>`_ framework, reading the fram
Useful snippets <useful-snippets.rst>
manual/index.rst
Assets <assets.rst>
Cron Jobs <cronjob.rst>
Layout and UI
**************

View File

@@ -18,7 +18,6 @@ Installation & Usage
:maxdepth: 2
prod.rst
load-addresses.rst
prod-calendar-sms-sending.rst
msgraph-configure.rst
@@ -171,7 +170,7 @@ There are several users available:
The password is always ``password``.
Now, read `Operations` below. For running in production, read `prod_`.
Now, read `Operations` below.
Operations

View File

@@ -1,50 +0,0 @@
.. _addresses:
Addresses
*********
Chill can store a list of geolocated address references, which are used to suggest address and ensure that the data is correctly stored.
Those addresses may be load from a dedicated source.
In France
=========
The address are loaded from the `BANO <https://bano.openstreetmap.fr/>`_. The postal codes are loaded from `the official list of
postal codes <https://datanova.laposte.fr/explore/dataset/laposte_hexasmal/information/>`_
.. code-block:: bash
# first, load postal codes
bin/console chill:main:postal-code:load:FR
# then, load all addresses, by departement (multiple departement can be loaded by repeating the departement code
bin/console chill:main:address-ref-from-bano 57 54 51
In Belgium
==========
Addresses are prepared from the `BeST Address data <https://www.geo.be/catalog/details/ca0fd5c0-8146-11e9-9012-482ae30f98d9>`_.
Postal code are loaded from this database. There is no need to load postal codes from another source (actually, this is strongly discouraged).
The data are prepared for Chill (`See this repository <https://gitea.champs-libres.be/Chill-project/belgian-bestaddresses-transform/releases>`_).
One can select postal code by his first number (:code:`1xxx` for postal codes from 1000 to 1999), or a limited list for development purpose.
.. code-block:: bash
# load postal code from 1000 to 3999:
bin/console chill:main:address-ref-from-best-addresse 1xxx 2xxx 3xxx
# load only an extract (for dev purposes)
bin/console chill:main:address-ref-from-best-addresse extract
# load full addresses (discouraged)
bin/console chill:main:address-ref-from-best-addresse full
.. note::
There is a possibility to load the full list of addresses is discouraged: the loading is optimized with smaller extracts.
Once you load the full list, it is not possible to load smaller extract: each extract loaded **after** will not
delete the addresses loaded with the full extract (and some addresses will be present twice).

View File

@@ -1,12 +1,10 @@
.. Copyright (C) 2014-2019 Champs Libres Cooperative SCRLFS
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
.. _prod:
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
Installation for production
###########################
@@ -38,25 +36,12 @@ This should be adapted to your needs:
* Think about how you will backup your database. Some adminsys find easier to store database outside of docker, which might be easier to administrate or replicate.
Cron jobs
=========
The command :code:`chill:cron-job:execute` should be executed every 15 minutes (more or less).
This command should never be executed concurrently. It should be not have more than one process for a single instance.
Post-install tasks
==================
- import addresses. See :ref:`addresses`.
Tweak symfony messenger
=======================
Calendar sync is processed using symfony messenger.
You can tweak the configuration
You can tweak the configuration
Going further:

View File

@@ -5,70 +5,69 @@ Add condition with distinct alias on each export join clauses (Indicators + Filt
These are alias conventions :
| Entity | Join | Attribute | Alias |
|:----------------------------------------|:----------------------------------------|:-------------------------------------------|:---------------------------------------|
| AccompanyingPeriod::class | | | acp |
| | AccompanyingPeriodWork::class | acp.works | acpw |
| | AccompanyingPeriodParticipation::class | acp.participations | acppart |
| | Location::class | acp.administrativeLocation | acploc |
| | ClosingMotive::class | acp.closingMotive | acpmotive |
| | UserJob::class | acp.job | acpjob |
| | Origin::class | acp.origin | acporigin |
| | Scope::class | acp.scopes | acpscope |
| | SocialIssue::class | acp.socialIssues | acpsocialissue |
| | User::class | acp.user | acpuser |
| | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories |
| AccompanyingPeriodWork::class | | | acpw |
| | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval |
| | User::class | acpw.referrers | acpwuser |
| | SocialAction::class | acpw.socialAction | acpwsocialaction |
| | Goal::class | acpw.goals | goal |
| | Result::class | acpw.results | result |
| AccompanyingPeriodParticipation::class | | | acppart |
| | Person::class | acppart.person | partperson |
| AccompanyingPeriodWorkEvaluation::class | | | workeval |
| | Evaluation::class | workeval.evaluation | eval |
| Goal::class | | | goal |
| | Result::class | goal.results | goalresult |
| Person::class | | | person |
| | Center::class | person.center | center |
| | HouseholdMember::class | partperson.householdParticipations | householdmember |
| | MaritalStatus::class | person.maritalStatus | personmarital |
| | VendeePerson::class | | vp |
| | VendeePersonMineur::class | | vpm |
| | CurrentPersonAddress::class | person.currentPersonAddress | currentPersonAddress (on a given date) |
| ResidentialAddress::class | | | resaddr |
| | ThirdParty::class | resaddr.hostThirdParty | tparty |
| ThirdParty::class | | | tparty |
| | ThirdPartyCategory::class | tparty.categories | tpartycat |
| HouseholdMember::class | | | householdmember |
| | Household::class | householdmember.household | household |
| | Person::class | householdmember.person | memberperson |
| | | memberperson.center | membercenter |
| Household::class | | | household |
| | HouseholdComposition::class | household.compositions | composition |
| Activity::class | | | activity |
| | Person::class | activity.person | actperson |
| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp |
| | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity |
| | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity |
| | ActivityType::class | activity.activityType | acttype |
| | Location::class | activity.location | actloc |
| | SocialAction::class | activity.socialActions | actsocialaction |
| | SocialIssue::class | activity.socialIssues | actsocialssue |
| | ThirdParty::class | activity.thirdParties | acttparty |
| | User::class | activity.user | actuser |
| | User::class | activity.users | actusers |
| | ActivityReason::class | activity.reasons | actreasons |
| | Center::class | actperson.center | actcenter |
| | Person::class | activity.createdBy | actcreator |
| ActivityReason::class | | | actreasons |
| | ActivityReasonCategory::class | actreason.category | actreasoncat |
| Calendar::class | | | cal |
| | CancelReason::class | cal.cancelReason | calcancel |
| | Location::class | cal.location | calloc |
| | User::class | cal.user | caluser |
| VendeePerson::class | | | vp |
| | SituationProfessionelle::class | vp.situationProfessionelle | vpprof |
| | StatutLogement::class | vp.statutLogement | vplog |
| | TempsDeTravail::class | vp.tempsDeTravail | vptt |
| Entity | Join | Attribute | Alias |
|:----------------------------------------|:----------------------------------------|:-------------------------------------------|:----------------------------------|
| AccompanyingPeriod::class | | | acp |
| | AccompanyingPeriodWork::class | acp.works | acpw |
| | AccompanyingPeriodParticipation::class | acp.participations | acppart |
| | Location::class | acp.administrativeLocation | acploc |
| | ClosingMotive::class | acp.closingMotive | acpmotive |
| | UserJob::class | acp.job | acpjob |
| | Origin::class | acp.origin | acporigin |
| | Scope::class | acp.scopes | acpscope |
| | SocialIssue::class | acp.socialIssues | acpsocialissue |
| | User::class | acp.user | acpuser |
| | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories |
| AccompanyingPeriodWork::class | | | acpw |
| | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval |
| | User::class | acpw.referrers | acpwuser |
| | SocialAction::class | acpw.socialAction | acpwsocialaction |
| | Goal::class | acpw.goals | goal |
| | Result::class | acpw.results | result |
| AccompanyingPeriodParticipation::class | | | acppart |
| | Person::class | acppart.person | partperson |
| AccompanyingPeriodWorkEvaluation::class | | | workeval |
| | Evaluation::class | workeval.evaluation | eval |
| Goal::class | | | goal |
| | Result::class | goal.results | goalresult |
| Person::class | | | person |
| | Center::class | person.center | center |
| | HouseholdMember::class | partperson.householdParticipations | householdmember |
| | MaritalStatus::class | person.maritalStatus | personmarital |
| | VendeePerson::class | | vp |
| | VendeePersonMineur::class | | vpm |
| ResidentialAddress::class | | | resaddr |
| | ThirdParty::class | resaddr.hostThirdParty | tparty |
| ThirdParty::class | | | tparty |
| | ThirdPartyCategory::class | tparty.categories | tpartycat |
| HouseholdMember::class | | | householdmember |
| | Household::class | householdmember.household | household |
| | Person::class | householdmember.person | memberperson |
| | | memberperson.center | membercenter |
| Household::class | | | household |
| | HouseholdComposition::class | household.compositions | composition |
| Activity::class | | | activity |
| | Person::class | activity.person | actperson |
| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp |
| | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity |
| | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity |
| | ActivityType::class | activity.activityType | acttype |
| | Location::class | activity.location | actloc |
| | SocialAction::class | activity.socialActions | actsocialaction |
| | SocialIssue::class | activity.socialIssues | actsocialssue |
| | ThirdParty::class | activity.thirdParties | acttparty |
| | User::class | activity.user | actuser |
| | User::class | activity.users | actusers |
| | ActivityReason::class | activity.reasons | actreasons |
| | Center::class | actperson.center | actcenter |
| | Person::class | activity.createdBy | actcreator |
| ActivityReason::class | | | actreasons |
| | ActivityReasonCategory::class | actreason.category | actreasoncat |
| Calendar::class | | | cal |
| | CancelReason::class | cal.cancelReason | calcancel |
| | Location::class | cal.location | calloc |
| | User::class | cal.user | caluser |
| VendeePerson::class | | | vp |
| | SituationProfessionelle::class | vp.situationProfessionelle | vpprof |
| | StatutLogement::class | vp.statutLogement | vplog |
| | TempsDeTravail::class | vp.tempsDeTravail | vptt |

View File

@@ -1,67 +0,0 @@
{
"name": "chill",
"version": "2.0.0",
"devDependencies": {
"@alexlafroscia/yaml-merge": "^4.0.0",
"@apidevtools/swagger-cli": "^4.0.4",
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@ckeditor/ckeditor5-build-classic": "^35.3.2",
"@ckeditor/ckeditor5-dev-utils": "^31.1.13",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13",
"@ckeditor/ckeditor5-markdown-gfm": "^35.3.2",
"@ckeditor/ckeditor5-theme-lark": "^35.3.2",
"@ckeditor/ckeditor5-vue": "^4.0.1",
"@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node14": "^1.0.1",
"bindings": "^1.5.0",
"bootstrap": "^5.0.1",
"chokidar": "^3.5.1",
"fork-awesome": "^1.1.7",
"jquery": "^3.6.0",
"node-sass": "^8.0.0",
"popper.js": "^1.16.1",
"postcss-loader": "^7.0.2",
"raw-loader": "^4.0.2",
"sass-loader": "^13.0.0",
"select2": "^4.0.13",
"select2-bootstrap-theme": "0.1.0-beta.10",
"style-loader": "^3.3.1",
"ts-loader": "^9.3.1",
"typescript": "^4.7.2",
"vue-loader": "^17.0.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
},
"dependencies": {
"@fullcalendar/core": "^5.11.0",
"@fullcalendar/daygrid": "^5.11.0",
"@fullcalendar/interaction": "^5.11.0",
"@fullcalendar/list": "^5.11.0",
"@fullcalendar/timegrid": "^5.11.0",
"@fullcalendar/vue3": "^5.11.1",
"@popperjs/core": "^2.9.2",
"dropzone": "^5.7.6",
"es6-promise": "^4.2.8",
"leaflet": "^1.7.1",
"masonry-layout": "^4.2.2",
"mime": "^3.0.0",
"swagger-ui": "^4.15.5",
"vis-network": "^9.1.0",
"vue": "^3.2.37",
"vue-i18n": "^9.1.6",
"vue-multiselect": "3.0.0-alpha.2",
"vue-toast-notification": "^2.0",
"vuex": "^4.0.0"
},
"browserslist": [
"Firefox ESR"
],
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
},
"private": true
}

View File

@@ -5,6 +5,11 @@ parameters:
count: 1
path: src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php
-
message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\Entity\\\\Household\\\\PersonHouseholdAddress\\:\\:\\$relation\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php
-
message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\Entity\\\\AccompanyingPeriod\\:\\:\\$work\\.$#"
count: 1
@@ -25,6 +30,11 @@ parameters:
count: 1
path: src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php
-
message: "#^Undefined variable\\: \\$choiceSlug$#"
count: 1
path: src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php
-
message: "#^Undefined variable\\: \\$choiceSlug$#"
count: 1

View File

@@ -10,6 +10,16 @@ parameters:
count: 1
path: src/Bundle/ChillActivityBundle/Entity/ActivityReasonCategory.php
-
message: "#^Method Chill\\\\ActivityBundle\\\\Export\\\\Export\\\\StatActivityDuration\\:\\:getDescription\\(\\) should return string but return statement is missing\\.$#"
count: 1
path: src/Bundle/ChillActivityBundle/Export/Export/StatActivityDuration.php
-
message: "#^Method Chill\\\\ActivityBundle\\\\Export\\\\Export\\\\StatActivityDuration\\:\\:getTitle\\(\\) should return string but return statement is missing\\.$#"
count: 1
path: src/Bundle/ChillActivityBundle/Export/Export/StatActivityDuration.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1
@@ -320,6 +330,21 @@ parameters:
count: 6
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Export/Aggregator/CountryOfBirthAggregator.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Export/Aggregator/NationalityAggregator.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 2
path: src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1

View File

@@ -363,7 +363,6 @@ final class ActivityController extends AbstractController
if ($person instanceof Person) {
$entity->setPerson($person);
$entity->getPersons()->add($person);
}
if ($accompanyingPeriod instanceof AccompanyingPeriod) {

View File

@@ -1,68 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Export\Aggregator\ACPAggregators;
use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class ByActivityNumberAggregator implements AggregatorInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb
->addSelect('(SELECT COUNT(activity.id) FROM ' . Activity::class . ' activity WHERE activity.accompanyingPeriod = acp) AS activity_by_number_aggregator')
->addGroupBy('activity_by_number_aggregator');
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder): void
{
// No form needed
}
public function getLabels($key, array $values, $data)
{
return static function ($value) {
if ('_header' === $value) {
return '';
}
if (null === $value) {
return '';
}
return $value;
};
}
public function getQueryKeys($data): array
{
return ['activity_by_number_aggregator'];
}
public function getTitle(): string
{
return 'Group acp by activity number';
}
}

View File

@@ -44,7 +44,7 @@ class ActivityTypeAggregator implements AggregatorInterface
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('acttype', $qb->getAllAliases(), true)) {
$qb->leftJoin('activity.activityType', 'acttype');
$qb->join('activity.activityType', 'acttype');
}
$qb->addSelect(sprintf('IDENTITY(activity.activityType) AS %s', self::KEY));

View File

@@ -1,83 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Export\Aggregator;
use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\AggregatorInterface;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class SentReceivedAggregator implements AggregatorInterface
{
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb->addSelect('activity.sentReceived AS activity_sentreceived_aggregator')
->addGroupBy('activity_sentreceived_aggregator');
}
public function applyOn(): string
{
return Declarations::ACTIVITY;
}
public function buildForm(FormBuilderInterface $builder): void
{
// No form needed
}
public function getLabels($key, array $values, $data): callable
{
return function (?string $value): string {
if ('_header' === $value) {
return 'export.aggregator.activity.by_sent_received.Sent or received';
}
switch ($value) {
case null:
return '';
case 'sent':
return $this->translator->trans('export.aggregator.activity.by_sent_received.is sent');
case 'received':
return $this->translator->trans('export.aggregator.activity.by_sent_received.is received');
default:
throw new LogicException(sprintf('The value %s is not valid', $value));
}
};
}
public function getQueryKeys($data): array
{
return ['activity_sentreceived_aggregator'];
}
public function getTitle(): string
{
return 'export.aggregator.activity.by_sent_received.Group activity by sentreceived';
}
}

View File

@@ -1,165 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Export\Export\LinkedToACP;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Export\Export\ListActivityHelper;
use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Export\Helper\TranslatableStringExportLabelHelper;
use Chill\MainBundle\Export\ListInterface;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormBuilderInterface;
class ListActivity implements ListInterface, GroupedExportInterface
{
private EntityManagerInterface $entityManager;
private ListActivityHelper $helper;
private TranslatableStringExportLabelHelper $translatableStringExportLabelHelper;
public function __construct(
ListActivityHelper $helper,
EntityManagerInterface $entityManager,
TranslatableStringExportLabelHelper $translatableStringExportLabelHelper
) {
$this->helper = $helper;
$this->entityManager = $entityManager;
$this->translatableStringExportLabelHelper = $translatableStringExportLabelHelper;
}
public function buildForm(FormBuilderInterface $builder)
{
$this->helper->buildForm($builder);
}
public function getAllowedFormattersTypes()
{
return $this->helper->getAllowedFormattersTypes();
}
public function getDescription()
{
return ListActivityHelper::MSG_KEY . 'List activities linked to an accompanying course';
}
public function getGroup(): string
{
return 'Exports of activities linked to an accompanying period';
}
public function getLabels($key, array $values, $data)
{
switch ($key) {
case 'acpId':
return static function ($value) {
if ('_header' === $value) {
return ListActivityHelper::MSG_KEY . 'accompanying course id';
}
return $value ?? '';
};
case 'scopesNames':
return $this->translatableStringExportLabelHelper->getLabelMulti($key, $values, ListActivityHelper::MSG_KEY . 'course circles');
default:
return $this->helper->getLabels($key, $values, $data);
}
}
public function getQueryKeys($data)
{
return
array_merge(
$this->helper->getQueryKeys($data),
[
'acpId',
'scopesNames',
]
);
}
public function getResult($query, $data)
{
return $this->helper->getResult($query, $data);
}
public function getTitle()
{
return ListActivityHelper::MSG_KEY . 'List activity linked to a course';
}
public function getType()
{
return $this->helper->getType();
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$centers = array_map(static function ($el) {
return $el['center'];
}, $acl);
$qb = $this->entityManager->createQueryBuilder();
$qb
->distinct()
->from(Activity::class, 'activity')
->join('activity.accompanyingPeriod', 'acp')
->leftJoin('acp.participations', 'acppart')
->leftJoin('acppart.person', 'person')
->andWhere('acppart.startDate != acppart.endDate OR acppart.endDate IS NULL')
->andWhere(
$qb->expr()->exists(
'SELECT 1
FROM ' . PersonCenterHistory::class . ' acl_count_person_history
WHERE acl_count_person_history.person = person
AND acl_count_person_history.center IN (:authorized_centers)
'
)
)
// some grouping are necessary
->addGroupBy('acp.id')
->addOrderBy('activity.date')
->addOrderBy('activity.id')
->setParameter('authorized_centers', $centers);
$this->helper->addSelect($qb);
// add select for this step
$qb
->addSelect('acp.id AS acpId')
->addSelect('(SELECT AGGREGATE(acpScope.name) FROM ' . Scope::class . ' acpScope WHERE acpScope MEMBER OF acp.scopes) AS scopesNames')
->addGroupBy('scopesNames');
return $qb;
}
public function requiredRole(): string
{
return ActivityStatsVoter::LISTS;
}
public function supportsModifiers()
{
return array_merge(
$this->helper->supportsModifiers(),
[
\Chill\PersonBundle\Export\Declarations::ACP_TYPE,
]
);
}
}

View File

@@ -124,7 +124,7 @@ class ListActivity implements ListInterface, GroupedExportInterface
return 'attendee';
}
return $value ? 'X' : '';
return $value ? 1 : 0;
};
case 'list_reasons':
@@ -210,20 +210,10 @@ class ListActivity implements ListInterface, GroupedExportInterface
$qb
->from('ChillActivityBundle:Activity', 'activity')
->join('activity.person', 'actperson')
->join('actperson.centerHistory', 'centerHistory');
$qb->where(
$qb->expr()->andX(
$qb->expr()->lte('centerHistory.startDate', 'activity.date'),
$qb->expr()->orX(
$qb->expr()->isNull('centerHistory.endDate'),
$qb->expr()->gt('centerHistory.endDate', 'activity.date')
)
)
)
->andWhere($qb->expr()->in('centerHistory.center', ':centers'))
->setParameter('centers', $centers);
->join('activity.person', 'person')
->join('actperson.center', 'actcenter')
->andWhere('actcenter IN (:authorized_centers)')
->setParameter('authorized_centers', $centers);
foreach ($this->fields as $f) {
if (in_array($f, $data['fields'], true)) {
@@ -234,17 +224,17 @@ class ListActivity implements ListInterface, GroupedExportInterface
break;
case 'person_firstname':
$qb->addSelect('actperson.firstName AS person_firstname');
$qb->addSelect('person.firstName AS person_firstname');
break;
case 'person_lastname':
$qb->addSelect('actperson.lastName AS person_lastname');
$qb->addSelect('person.lastName AS person_lastname');
break;
case 'person_id':
$qb->addSelect('actperson.id AS person_id');
$qb->addSelect('person.id AS person_id');
break;
@@ -261,7 +251,7 @@ class ListActivity implements ListInterface, GroupedExportInterface
break;
case 'type_name':
$qb->join('activity.activityType', 'type');
$qb->join('activity.type', 'type');
$qb->addSelect('type.name AS type_name');
break;
@@ -273,11 +263,6 @@ class ListActivity implements ListInterface, GroupedExportInterface
break;
case 'attendee':
$qb->addSelect('IDENTITY(activity.attendee) AS attendee');
break;
default:
$qb->addSelect(sprintf('activity.%s as %s', $f, $f));

View File

@@ -1,269 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Export\Export;
use Chill\ActivityBundle\Export\Declarations;
use Chill\ActivityBundle\Repository\ActivityPresenceRepositoryInterface;
use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\Helper\DateTimeHelper;
use Chill\MainBundle\Export\Helper\TranslatableStringExportLabelHelper;
use Chill\MainBundle\Export\Helper\UserHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use const SORT_NUMERIC;
class ListActivityHelper
{
public const MSG_KEY = 'export.list.activity.';
private ActivityPresenceRepositoryInterface $activityPresenceRepository;
private ActivityTypeRepositoryInterface $activityTypeRepository;
private DateTimeHelper $dateTimeHelper;
private LabelPersonHelper $labelPersonHelper;
private LabelThirdPartyHelper $labelThirdPartyHelper;
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatableStringExportLabelHelper $translatableStringLabelHelper;
private TranslatorInterface $translator;
private UserHelper $userHelper;
public function __construct(
ActivityPresenceRepositoryInterface $activityPresenceRepository,
ActivityTypeRepositoryInterface $activityTypeRepository,
DateTimeHelper $dateTimeHelper,
LabelPersonHelper $labelPersonHelper,
LabelThirdPartyHelper $labelThirdPartyHelper,
TranslatorInterface $translator,
TranslatableStringHelperInterface $translatableStringHelper,
TranslatableStringExportLabelHelper $translatableStringLabelHelper,
UserHelper $userHelper
) {
$this->activityPresenceRepository = $activityPresenceRepository;
$this->activityTypeRepository = $activityTypeRepository;
$this->dateTimeHelper = $dateTimeHelper;
$this->labelPersonHelper = $labelPersonHelper;
$this->labelThirdPartyHelper = $labelThirdPartyHelper;
$this->translator = $translator;
$this->translatableStringHelper = $translatableStringHelper;
$this->translatableStringLabelHelper = $translatableStringLabelHelper;
$this->userHelper = $userHelper;
}
public function addSelect(QueryBuilder $qb): void
{
$qb
->addSelect('activity.id AS id')
->addSelect('activity.date')
->addSelect('IDENTITY(activity.activityType) AS typeName')
->leftJoin('activity.reasons', 'reasons')
->addSelect('AGGREGATE(reasons.name) AS listReasons')
->leftJoin('activity.persons', 'actPerson')
->addSelect('AGGREGATE(actPerson.id) AS personsIds')
->addSelect('AGGREGATE(actPerson.id) AS personsNames')
->leftJoin('activity.users', 'users_u')
->addSelect('AGGREGATE(users_u.id) AS usersIds')
->addSelect('AGGREGATE(users_u.id) AS usersNames')
->leftJoin('activity.thirdParties', 'thirdparty')
->addSelect('AGGREGATE(thirdparty.id) AS thirdPartiesIds')
->addSelect('AGGREGATE(thirdparty.id) AS thirdPartiesNames')
->addSelect('IDENTITY(activity.attendee) AS attendeeName')
->addSelect('activity.durationTime')
->addSelect('activity.travelTime')
->addSelect('activity.emergency')
->leftJoin('activity.location', 'location')
->addSelect('location.name AS locationName')
->addSelect('activity.sentReceived')
->addSelect('IDENTITY(activity.createdBy) AS createdBy')
->addSelect('activity.createdAt')
->addSelect('IDENTITY(activity.updatedBy) AS updatedBy')
->addSelect('activity.updatedAt')
->addGroupBy('activity.id')
->addGroupBy('location.id');
}
public function buildForm(FormBuilderInterface $builder)
{
}
public function getAllowedFormattersTypes()
{
return [FormatterInterface::TYPE_LIST];
}
public function getLabels($key, array $values, $data)
{
switch ($key) {
case 'createdAt':
case 'updatedAt':
return $this->dateTimeHelper->getLabel($key);
case 'createdBy':
case 'updatedBy':
return $this->userHelper->getLabel($key, $values, $key);
case 'date':
return $this->dateTimeHelper->getLabel(self::MSG_KEY . $key);
case 'attendeeName':
return function ($value) {
if ('_header' === $value) {
return 'Attendee';
}
if (null === $value || null === $presence = $this->activityPresenceRepository->find($value)) {
return '';
}
return $this->translatableStringHelper->localize($presence->getName());
};
case 'listReasons':
return $this->translatableStringLabelHelper->getLabelMulti($key, $values, 'Activity Reasons');
case 'typeName':
return function ($value) {
if ('_header' === $value) {
return 'Activity type';
}
if (null === $value || null === $type = $this->activityTypeRepository->find($value)) {
return '';
}
return $this->translatableStringHelper->localize($type->getName());
};
case 'usersNames':
return $this->userHelper->getLabelMulti($key, $values, self::MSG_KEY . 'users name');
case 'usersIds':
case 'thirdPartiesIds':
case 'personsIds':
return static function ($value) use ($key) {
if ('_header' === $value) {
switch ($key) {
case 'usersIds':
return self::MSG_KEY . 'users ids';
case 'thirdPartiesIds':
return self::MSG_KEY . 'third parties ids';
case 'personsIds':
return self::MSG_KEY . 'persons ids';
default:
throw new LogicException('key not supported');
}
}
$decoded = json_decode($value);
return implode(
'|',
array_unique(
array_filter($decoded, static fn (?int $id) => null !== $id),
SORT_NUMERIC
)
);
};
case 'personsNames':
return $this->labelPersonHelper->getLabelMulti($key, $values, self::MSG_KEY . 'persons name');
case 'thirdPartiesNames':
return $this->labelThirdPartyHelper->getLabelMulti($key, $values, self::MSG_KEY . 'thirds parties');
case 'sentReceived':
return function ($value) {
if ('_header' === $value) {
return self::MSG_KEY . 'sent received';
}
if (null === $value) {
return '';
}
return $this->translator->trans($value);
};
default:
return function ($value) use ($key) {
if ('_header' === $value) {
return self::MSG_KEY . $key;
}
if (null === $value) {
return '';
}
return $this->translator->trans($value);
};
}
}
public function getQueryKeys($data)
{
return [
'id',
'date',
'typeName',
'listReasons',
'attendeeName',
'durationTime',
'travelTime',
'emergency',
'locationName',
'sentReceived',
'personsIds',
'personsNames',
'usersIds',
'usersNames',
'thirdPartiesIds',
'thirdPartiesNames',
'createdBy',
'createdAt',
'updatedBy',
'updatedAt',
];
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(AbstractQuery::HYDRATE_SCALAR);
}
public function getType(): string
{
return Declarations::ACTIVITY;
}
public function supportsModifiers()
{
return [
Declarations::ACTIVITY,
];
}
}

View File

@@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Export\Filter\ACPFilters;
use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Export\FilterInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class HasNoActivityFilter implements FilterInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb
->andWhere('
NOT EXISTS (
SELECT 1 FROM ' . Activity::class . ' activity
WHERE activity.accompanyingPeriod = acp
)
');
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
//no form needed
}
public function describeAction($data, $format = 'string'): array
{
return ['Filtered acp which has no activities', []];
}
public function getTitle(): string
{
return 'Filter acp which has no activity';
}
}

View File

@@ -13,10 +13,9 @@ namespace Chill\ActivityBundle\Export\Filter;
use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\Export\FilterType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use DateTime;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
@@ -29,14 +28,9 @@ class ActivityDateFilter implements FilterInterface
{
protected TranslatorInterface $translator;
private RollingDateConverterInterface $rollingDateConverter;
public function __construct(
TranslatorInterface $translator,
RollingDateConverterInterface $rollingDateConverter
) {
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
$this->rollingDateConverter = $rollingDateConverter;
}
public function addRole(): ?string
@@ -60,14 +54,8 @@ class ActivityDateFilter implements FilterInterface
}
$qb->add('where', $where);
$qb->setParameter(
'date_from',
$this->rollingDateConverter->convert($data['date_from'])
);
$qb->setParameter(
'date_to',
$this->rollingDateConverter->convert($data['date_to'])
);
$qb->setParameter('date_from', $data['date_from']);
$qb->setParameter('date_to', $data['date_to']);
}
public function applyOn(): string
@@ -78,13 +66,13 @@ class ActivityDateFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('date_from', PickRollingDateType::class, [
->add('date_from', ChillDateType::class, [
'label' => 'Activities after this date',
'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
'data' => new DateTime(),
])
->add('date_to', PickRollingDateType::class, [
->add('date_to', ChillDateType::class, [
'label' => 'Activities before this date',
'data' => new RollingDate(RollingDate::T_TODAY),
'data' => new DateTime(),
]);
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
@@ -133,8 +121,8 @@ class ActivityDateFilter implements FilterInterface
return [
'Filtered by date of activity: only between %date_from% and %date_to%',
[
'%date_from%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'),
'%date_to%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'),
'%date_from%' => $data['date_from']->format('d-m-Y'),
'%date_to%' => $data['date_to']->format('d-m-Y'),
],
];
}

View File

@@ -39,9 +39,9 @@ class UsersJobFilter implements FilterInterface
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . Activity::class . ' activity_users_job_filter_act
'SELECT 1 FROM ' . Activity::class . ' activity_users_job_filter_act
JOIN activity_users_job_filter_act.users users WHERE users.userJob IN (:activity_users_job_filter_jobs) AND activity_users_job_filter_act = activity '
)
)
)
->setParameter('activity_users_job_filter_jobs', $data['jobs']);
}

View File

@@ -1,51 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\ActivityPresence;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
class ActivityPresenceRepository implements ActivityPresenceRepositoryInterface
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository($this->getClassName());
}
public function find($id): ?ActivityPresence
{
return $this->repository->find($id);
}
public function findAll(): array
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?ActivityPresence
{
return $this->findOneBy($criteria);
}
public function getClassName(): string
{
return ActivityPresence::class;
}
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\ActivityPresence;
interface ActivityPresenceRepositoryInterface
{
public function find($id): ?ActivityPresence;
/**
* @return array|ActivityPresence[]
*/
public function findAll(): array;
/**
* @return array|ActivityPresence[]
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria): ?ActivityPresence;
public function getClassName(): string;
}

View File

@@ -117,8 +117,7 @@ export default {
target: { //name, id
},
edit: false,
addressId: null,
defaults: window.addaddress
addressId: null
}
}
}

View File

@@ -30,7 +30,7 @@ const store = createStore({
},
getters: {
suggestedEntities(state) {
if (typeof state.activity.accompanyingPeriod === "undefined" || state.activity.accompanyingPeriod === null) {
if (typeof state.activity.accompanyingPeriod === "undefined") {
return [];
}
const allEntities = [

View File

@@ -39,9 +39,6 @@ const makeConcernedThirdPartiesLocation = (locationType, store) => {
return locations;
};
const makeAccompanyingPeriodLocation = (locationType, store) => {
if (store.state.activity.accompanyingPeriod === null) {
return {};
}
const accPeriodLocation = store.state.activity.accompanyingPeriod.location;
return {
type: 'location',

View File

@@ -369,12 +369,8 @@ final class ActivityControllerTest extends WebTestCase
$center
);
$reachableScopesId = array_intersect(
array_map(static function ($s) {
return $s->getId();
}, $reachableScopesDelete),
array_map(static function ($s) {
return $s->getId();
}, $reachableScopesUpdate)
array_map(static function ($s) { return $s->getId(); }, $reachableScopesDelete),
array_map(static function ($s) { return $s->getId(); }, $reachableScopesUpdate)
);
if (count($reachableScopesId) === 0) {

View File

@@ -188,9 +188,7 @@ final class ActivityTypeTest extends KernelTestCase
// map all the values in an array
$values = array_map(
static function ($choice) {
return $choice->value;
},
static function ($choice) { return $choice->value; },
$view['activity']['durationTime']->vars['choices']
);

View File

@@ -41,12 +41,6 @@ services:
tags:
- { name: chill.export, alias: 'avg_activity_visit_duration_linked_to_acp' }
Chill\ActivityBundle\Export\Export\LinkedToACP\ListActivity:
tags:
- { name: chill.export, alias: 'list_activity_acp'}
Chill\ActivityBundle\Export\Export\ListActivityHelper: ~
## Filters
chill.activity.export.type_filter:
class: Chill\ActivityBundle\Export\Filter\ActivityTypeFilter
@@ -126,18 +120,15 @@ services:
tags:
- { name: chill.export_filter, alias: 'activity_usersscope_filter' }
Chill\ActivityBundle\Export\Filter\ACPFilters\HasNoActivityFilter:
tags:
- { name: chill.export_filter, alias: 'accompanyingcourse_has_no_activity_filter' }
## Aggregators
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator:
tags:
- { name: chill.export_aggregator, alias: activity_reason_aggregator }
Chill\ActivityBundle\Export\Aggregator\ActivityTypeAggregator:
chill.activity.export.type_aggregator:
class: Chill\ActivityBundle\Export\Aggregator\ActivityTypeAggregator
tags:
- { name: chill.export_aggregator, alias: activity_common_type_aggregator }
- { name: chill.export_aggregator, alias: activity_type_aggregator }
chill.activity.export.user_aggregator:
class: Chill\ActivityBundle\Export\Aggregator\ActivityUserAggregator
@@ -188,11 +179,3 @@ services:
Chill\ActivityBundle\Export\Aggregator\ActivityUsersJobAggregator:
tags:
- { name: chill.export_aggregator, alias: activity_users_job_aggregator }
Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByActivityNumberAggregator:
tags:
- { name: chill.export_aggregator, alias: accompanyingcourse_by_activity_number_aggregator }
Chill\ActivityBundle\Export\Aggregator\SentReceivedAggregator:
tags:
- { name: chill.export_aggregator, alias: activity_sentreceived_aggregator }

View File

@@ -45,8 +45,6 @@ by: 'Par '
location: Lieu
Reasons: Sujets
Private comment: Commentaire privé
sent: Envoyé
received: Reçu
#forms
@@ -262,6 +260,8 @@ activity is not emergency: l'activité n'est pas urgente
Filter activity by sentreceived: Filtrer les activités par envoyé/reçu
'Filtered activity by sentreceived: only %sentreceived%': "Filtré par envoyé/reçu: uniquement %sentreceived%"
Accepted sentreceived: ''
is sent: envoyé
is received: reçu
Filter activity by linked socialaction: Filtrer les activités par action liée
'Filtered activity by linked socialaction: only %actions%': "Filtré par action liée: uniquement %actions%"
Filter activity by linked socialissue: Filtrer les activités par problématique liée
@@ -276,10 +276,6 @@ Creators: Créateurs
Filter activity by userscope: Filtrer les activités par service du créateur
'Filtered activity by userscope: only %scopes%': "Filtré par service du créateur: uniquement %scopes%"
Accepted userscope: Services
Filter acp which has no activity: Filtrer les parcours qui nont pas dactivité
Filtered acp which has no activities: Filtrer les parcours sans activité associée
Group acp by activity number: Grouper les parcours par nombre dactivité
#aggregators
Activity type: Type d'activité
@@ -323,32 +319,11 @@ This is the minimal activity data: Activité n°
docgen:
Activity basic: Echange
A basic context for activity: Contexte pour les activités
Accompanying period with a list of activities: Parcours d'accompagnement avec liste des activités
Accompanying period with a list of activities description: Ce contexte reprend les informations du parcours, et tous les activités pour un parcours. Les activités ne sont pas filtrés.
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.
export:
list:
activity:
users name: Nom des utilisateurs
users ids: Identifiant des utilisateurs
third parties ids: Identifiant des tiers
persons ids: Identifiant des personnes
persons name: Nom des personnes
thirds parties: Tiers
date: Date de l'activité
locationName: Localisation
sent received: Envoyé ou reçu
emergency: Urgence
accompanying course id: Identifiant du parcours
course circles: Cercles du parcours
travelTime: Durée de déplacement
durationTime: Durée
id: Identifiant
List activities linked to an accompanying course: Liste les activités liées à un parcours en fonction de différents filtres.
List activity linked to a course: Liste des activités liées à un parcours
filter:
activity:
by_usersjob:
@@ -357,10 +332,3 @@ export:
by_usersscope:
Filter by users scope: Filtrer les activités par services d'au moins un utilisateur participant
'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%'
aggregator:
activity:
by_sent_received:
Sent or received: Envoyé ou reçu
is sent: envoyé
is received: reçu
Group activity by sentreceived: Grouper les activités par envoyé / reçu

View File

@@ -231,4 +231,4 @@ This is the minimal activity data: Activité n°
docgen:
Activity basic: Echange
A basic context for activity: Contexte pour les activités
A basic context for activity: Contexte pour les échanges

View File

@@ -16,8 +16,8 @@ For this type of activity, document is required: Pour ce type d'activité, un do
For this type of activity, emergency is required: Pour ce type d'activité, le champ "Urgent" est requis
For this type of activity, accompanying period is required: Pour ce type d'activité, le parcours d'accompagnement est requis
For this type of activity, you must add at least one social issue: Pour ce type d'activité, vous devez ajouter au moins une problématique sociale
For this type of activity, you must add at least one social action: Pour ce type d'activité, vous devez indiquer au moins une action sociale
For this type of activity, you must add at least one social action: Pour ce type d'activité, vous devez indiquez au moins une action sociale
# admin
This parameter must be equal to social issue parameter: Ce paramètre doit être égal au paramètre "Visibilité du champs Problématiques sociales"
The socialActionsVisible value is not compatible with the socialIssuesVisible value: Cette valeur du paramètre "Visibilité du champs Actions sociales" n'est pas compatible avec la valeur du paramètre "Visibilité du champs Problématiques sociales"
The socialActionsVisible value is not compatible with the socialIssuesVisible value: Cette valeur du paramètre "Visibilité du champs Actions sociales" n'est pas compatible avec la valeur du paramètre "Visibilité du champs Problématiques sociales"

View File

@@ -30,8 +30,6 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte
$loader->load('services.yaml');
$loader->load('services/form.yaml');
$loader->load('services/menu.yaml');
$loader->load('services/security.yaml');
$loader->load('services/export.yaml');
}
public function prepend(ContainerBuilder $container)

View File

@@ -1,82 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\AsideActivityBundle\Export\Aggregator;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\AsideActivityBundle\Repository\AsideActivityCategoryRepository;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class ByActivityTypeAggregator implements AggregatorInterface
{
private AsideActivityCategoryRepository $asideActivityCategoryRepository;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(AsideActivityCategoryRepository $asideActivityCategoryRepository, TranslatableStringHelper $translatableStringHelper)
{
$this->asideActivityCategoryRepository = $asideActivityCategoryRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb->addSelect('IDENTITY(aside.type) AS by_aside_activity_type_aggregator')
->addGroupBy('by_aside_activity_type_aggregator');
}
public function applyOn(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
// No form needed
}
public function getLabels($key, array $values, $data)
{
$this->asideActivityCategoryRepository->findBy(['id' => $values]);
return function ($value): string {
if ('_header' === $value) {
return 'export.aggregator.Aside activity type';
}
if (null === $value) {
return '';
}
$t = $this->asideActivityCategoryRepository->find($value);
return $this->translatableStringHelper->localize($t->getTitle());
};
}
public function getQueryKeys($data): array
{
return ['by_aside_activity_type_aggregator'];
}
public function getTitle(): string
{
return 'export.aggregator.Group by aside activity type';
}
}

View File

@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\AsideActivityBundle\Export;
/**
* This class declare constants used for the export framework.
*/
abstract class Declarations
{
public const ASIDE_ACTIVITY_TYPE = 'aside_activity';
}

View File

@@ -1,105 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\AsideActivityBundle\Export\Export;
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use ChillAsideActivityBundle\Export\Declarations;
use Doctrine\ORM\Query;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
class CountAsideActivity implements ExportInterface, GroupedExportInterface
{
private AsideActivityRepository $repository;
public function __construct(
AsideActivityRepository $repository
) {
$this->repository = $repository;
}
public function buildForm(FormBuilderInterface $builder)
{
}
public function getAllowedFormattersTypes(): array
{
return [FormatterInterface::TYPE_TABULAR];
}
public function getDescription(): string
{
return 'export.Count aside activities by various parameters.';
}
public function getGroup(): string
{
return 'export.Exports of aside activities';
}
public function getLabels($key, array $values, $data)
{
if ('export_result' !== $key) {
throw new LogicException("the key {$key} is not used by this export");
}
$labels = array_combine($values, $values);
$labels['_header'] = $this->getTitle();
return static function ($value) use ($labels) {
return $labels[$value];
};
}
public function getQueryKeys($data): array
{
return ['export_result'];
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
}
public function getTitle(): string
{
return 'export.Count aside activities';
}
public function getType(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$qb = $this->repository->createQueryBuilder('aside');
$qb->select('COUNT(DISTINCT aside.id) AS export_result');
return $qb;
}
public function requiredRole(): string
{
return AsideActivityVoter::STATS;
}
public function supportsModifiers(): array
{
return [];
}
}

View File

@@ -1,96 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\AsideActivityBundle\Export\Filter;
use Chill\AsideActivityBundle\Entity\AsideActivityCategory;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\AsideActivityBundle\Repository\AsideActivityCategoryRepository;
use Chill\AsideActivityBundle\Templating\Entity\CategoryRender;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
class ByActivityTypeFilter implements FilterInterface
{
private AsideActivityCategoryRepository $asideActivityTypeRepository;
private CategoryRender $categoryRender;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(
CategoryRender $categoryRender,
TranslatableStringHelperInterface $translatableStringHelper,
AsideActivityCategoryRepository $asideActivityTypeRepository
) {
$this->categoryRender = $categoryRender;
$this->asideActivityTypeRepository = $asideActivityTypeRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$clause = $qb->expr()->in('aside.type', ':types');
$qb->andWhere($clause);
$qb->setParameter('types', $data['types']);
}
public function applyOn(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('types', EntityType::class, [
'class' => AsideActivityCategory::class,
'choices' => $this->asideActivityTypeRepository->findAllActive(),
'required' => false,
'multiple' => true,
'expanded' => false,
'attr' => [
'class' => 'select2',
],
'choice_label' => function (AsideActivityCategory $category) {
$options = [];
return $this->categoryRender->renderString($category, $options);
},
]);
}
public function describeAction($data, $format = 'string'): array
{
$types = array_map(
fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getName()),
$this->asideActivityTypeRepository->findBy(['id' => $data['types']->toArray()])
);
return ['export.filter.Filtered by aside activity type: only %type%', [
'%type%' => implode(', ', $types),
]];
}
public function getTitle(): string
{
return 'export.filter.Filter by aside activity type';
}
}

View File

@@ -1,143 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\AsideActivityBundle\Export\Filter;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\Export\FilterType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Contracts\Translation\TranslatorInterface;
class ByDateFilter implements FilterInterface
{
protected TranslatorInterface $translator;
private RollingDateConverterInterface $rollingDateConverter;
public function __construct(
RollingDateConverterInterface $rollingDateConverter,
TranslatorInterface $translator
) {
$this->translator = $translator;
$this->rollingDateConverter = $rollingDateConverter;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->between(
'aside.date',
':date_from',
':date_to'
);
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->add('where', $where);
$qb->setParameter(
'date_from',
$this->rollingDateConverter->convert($data['date_from'])
);
$qb->setParameter(
'date_to',
$this->rollingDateConverter->convert($data['date_to'])
);
}
public function applyOn(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('date_from', PickRollingDateType::class, [
'label' => 'export.filter.Aside activities after this date',
'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
])
->add('date_to', PickRollingDateType::class, [
'label' => 'export.filter.Aside activities before this date',
'data' => new RollingDate(RollingDate::T_TODAY),
]);
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
/** @var \Symfony\Component\Form\FormInterface $filterForm */
$filterForm = $event->getForm()->getParent();
$enabled = $filterForm->get(FilterType::ENABLED_FIELD)->getData();
if (true === $enabled) {
// if the filter is enabled, add some validation
$form = $event->getForm();
$date_from = $form->get('date_from')->getData();
$date_to = $form->get('date_to')->getData();
// check that fields are not empty
if (null === $date_from) {
$form->get('date_from')->addError(new FormError(
$this->translator->trans('This field '
. 'should not be empty')
));
}
if (null === $date_to) {
$form->get('date_to')->addError(new FormError(
$this->translator->trans('This field '
. 'should not be empty')
));
}
// check that date_from is before date_to
if (
(null !== $date_from && null !== $date_to)
&& $date_from >= $date_to
) {
$form->get('date_to')->addError(new FormError(
$this->translator->trans('export.filter.This date should be after '
. 'the date given in "Implied in an aside activity after '
. 'this date" field')
));
}
}
});
}
public function describeAction($data, $format = 'string'): array
{
return ['export.filter.Filtered by aside activities between %dateFrom% and %dateTo%', [
'%dateFrom%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'),
'%dateTo%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'),
]];
}
public function getTitle(): string
{
return 'export.filter.Filter by aside activity date';
}
}

View File

@@ -38,11 +38,6 @@ class AsideActivityCategoryRepository implements ObjectRepository
return $this->repository->findAll();
}
public function findAllActive(): array
{
return $this->repository->findBy(['isActive' => true]);
}
/**
* @param mixed|null $limit
* @param mixed|null $offset

View File

@@ -12,20 +12,46 @@ declare(strict_types=1);
namespace Chill\AsideActivityBundle\Repository;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
/**
* @method AsideActivity|null find($id, $lockMode = null, $lockVersion = null)
* @method AsideActivity|null findOneBy(array $criteria, array $orderBy = null)
* @method AsideActivity[] findAll()
* @method AsideActivity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
final class AsideActivityRepository extends ServiceEntityRepository
final class AsideActivityRepository implements ObjectRepository
{
public function __construct(ManagerRegistry $registry)
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct($registry, AsideActivity::class);
$this->repository = $entityManager->getRepository(AsideActivity::class);
}
public function find($id): ?AsideActivity
{
return $this->repository->find($id);
}
/**
* @return AsideActivity[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return AsideActivity[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?AsideActivity
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string

View File

@@ -1,79 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\AsideActivityBundle\Security;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class AsideActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
public const STATS = 'CHILL_ASIDE_ACTIVITY_STATS';
private VoterHelperInterface $voterHelper;
public function __construct(
VoterHelperFactoryInterface $voterHelperFactory
) {
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(Center::class, [self::STATS])
->build();
}
/**
* @return string[]
*/
public function getRoles(): array
{
return $this->getAttributes();
}
/**
* @return string[][]
*/
public function getRolesWithHierarchy(): array
{
return ['Aside activity' => $this->getRoles()];
}
/**
* @return string[]
*/
public function getRolesWithoutScope(): array
{
return $this->getAttributes();
}
protected function supports($attribute, $subject)
{
return $this->voterHelper->supports($attribute, $subject);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
if (!$token->getUser() instanceof User) {
return false;
}
return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
}
private function getAttributes(): array
{
return [self::STATS];
}
}

View File

@@ -20,33 +20,3 @@ services:
resource: "../Controller"
autowire: true
autoconfigure: true
## Exports
# indicators
Chill\AsideActivityBundle\Export\Export\CountAsideActivity:
autowire: true
autoconfigure: true
tags:
- { name: chill.export, alias: count_asideactivity }
# filters
Chill\AsideActivityBundle\Export\Filter\ByDateFilter:
autowire: true
autoconfigure: true
tags:
- { name: chill.export_filter, alias: asideactivity_bydate_filter }
Chill\AsideActivityBundle\Export\Filter\ByActivityTypeFilter:
autowire: true
autoconfigure: true
tags:
- { name: chill.export_filter, alias: asideactivity_activitytype_filter }
# aggregators
Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator:
autowire: true
autoconfigure: true
tags:
- { name: chill.export_aggregator, alias: asideactivity_activitytype_aggregator }

View File

@@ -1,27 +0,0 @@
services:
_defaults:
autowire: true
autoconfigure: true
## Indicators
Chill\AsideActivityBundle\Export\Export\CountAsideActivity:
tags:
- { name: chill.export, alias: 'count_aside_activity' }
## Filters
chill.aside_activity.export.date_filter:
class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
tags:
- { name: chill.export_filter, alias: 'aside_activity_date_filter' }
chill.aside_activity.export.type_filter:
class: Chill\AsideActivityBundle\Export\Filter\ByActivityTypeFilter
tags:
- { name: chill.export_filter, alias: 'aside_activity_type_filter' }
## Aggregators
chill.aside_activity.export.type_aggregator:
class: Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator
tags:
- { name: chill.export_aggregator, alias: activity_type_aggregator }

View File

@@ -1,7 +0,0 @@
services:
Chill\AsideActivityBundle\Security\AsideActivityVoter:
autowire: true
autoconfigure: true
tags:
- { name: security.voter }
- { name: chill.role }

View File

@@ -166,21 +166,3 @@ Aside activities: Activités annexes
Aside activity types: Types d'activités annexes
Aside activity type configuration: Configuration des categories d'activités annexes
Aside activity configuration: Configuration des activités annexes
# exports
export:
Exports of aside activities: Exports des activités annexes
Count aside activities: Nombre d'activités annexes
Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
filter:
Filter by aside activity date: Filtrer les activités annexes par date
Filter by aside activity type: Filtrer les activités annexes par type d'activité
'Filtered by aside activity type: only %type%': "Filtré par type d'activité annexe: uniquement %type%"
This date should be after the date given in "Implied in an aside activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités annexes après cette date"
Aside activities after this date: Actvitités annexes après cette date
Aside activities before this date: Actvitités annexes avant cette date
aggregator:
Group by aside activity type: Grouper les activités annexes par type d'activité
Aside activity type: Type d'activité annexe

View File

@@ -31,9 +31,7 @@ class ConfigRepository
public function getChargesKeys(bool $onlyActive = false): array
{
return array_map(static function ($element) {
return $element['key'];
}, $this->getCharges($onlyActive));
return array_map(static function ($element) { return $element['key']; }, $this->getCharges($onlyActive));
}
/**
@@ -52,9 +50,7 @@ class ConfigRepository
public function getResourcesKeys(bool $onlyActive = false): array
{
return array_map(static function ($element) {
return $element['key'];
}, $this->getResources($onlyActive));
return array_map(static function ($element) { return $element['key']; }, $this->getResources($onlyActive));
}
/**
@@ -74,18 +70,14 @@ class ConfigRepository
private function getCharges(bool $onlyActive = false): array
{
return $onlyActive ?
array_filter($this->charges, static function ($el) {
return $el['active'];
})
array_filter($this->charges, static function ($el) { return $el['active']; })
: $this->charges;
}
private function getResources(bool $onlyActive = false): array
{
return $onlyActive ?
array_filter($this->resources, static function ($el) {
return $el['active'];
})
array_filter($this->resources, static function ($el) { return $el['active']; })
: $this->resources;
}

View File

@@ -150,7 +150,7 @@ abstract class AbstractElement
return $this;
}
public function setHousehold(?Household $household): self
public function setHousehold(Household $household): self
{
$this->household = $household;

View File

@@ -11,8 +11,8 @@ declare(strict_types=1);
namespace Chill\BudgetBundle\Entity;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\HasCenterInterface;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
@@ -22,7 +22,7 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\Table(name="chill_budget.charge")
* @ORM\Entity(repositoryClass="Chill\BudgetBundle\Repository\ChargeRepository")
*/
class Charge extends AbstractElement implements HasCentersInterface
class Charge extends AbstractElement implements HasCenterInterface
{
public const HELP_ASKED = 'running';
@@ -46,24 +46,22 @@ class Charge extends AbstractElement implements HasCentersInterface
private $help = self::HELP_NOT_RELEVANT;
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private ?int $id = null;
private $id;
public function __construct()
{
$this->setStartDate(new DateTimeImmutable('today'));
}
public function getCenters(): array
public function getCenter(): ?Center
{
if (null !== $this->getPerson()) {
return [$this->getPerson()->getCenter()];
}
return $this->getHousehold()->getCurrentPersons()->map(static fn (Person $p) => $p->getCenter())->toArray();
return $this->getPerson()->getCenter();
}
public function getHelp()

View File

@@ -11,8 +11,8 @@ declare(strict_types=1);
namespace Chill\BudgetBundle\Entity;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\HasCenterInterface;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
@@ -22,27 +22,25 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\Table(name="chill_budget.resource")
* @ORM\Entity(repositoryClass="Chill\BudgetBundle\Repository\ResourceRepository")
*/
class Resource extends AbstractElement implements HasCentersInterface
class Resource extends AbstractElement implements HasCenterInterface
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private ?int $id = null;
private $id;
public function __construct()
{
$this->setStartDate(new DateTimeImmutable('today'));
}
public function getCenters(): array
public function getCenter(): ?Center
{
if (null !== $this->getPerson()) {
return [$this->getPerson()->getCenter()];
}
return $this->getHousehold()->getCurrentPersons()->map(static fn (Person $p) => $p->getCenter())->toArray();
return $this->getPerson()->getCenter();
}
/**

View File

@@ -20,7 +20,7 @@ use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use UnexpectedValueException;
use function in_array;
class BudgetElementVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
@@ -68,29 +68,12 @@ class BudgetElementVoter extends AbstractChillVoter implements ProvideRoleHierar
protected function supports($attribute, $subject)
{
return $this->voter->supports($attribute, $subject);
return (in_array($attribute, self::ROLES, true) && $subject instanceof AbstractElement)
|| (($subject instanceof Person || $subject instanceof Household) && in_array($attribute, [self::SEE, self::CREATE], true));
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
if (
$subject instanceof Person
|| ($subject instanceof AbstractElement && null !== $person = $subject->getPerson())) {
return $this->voter->voteOnAttribute($attribute, $person ?? $subject, $token);
}
if (
$subject instanceof Household
|| ($subject instanceof AbstractElement && null !== $household = $subject->getHousehold())) {
foreach (($household ?? $subject)->getCurrentPersons() as $person) {
if ($this->voter->voteOnAttribute($attribute, $person, $token)) {
return true;
}
}
return false;
}
throw new UnexpectedValueException('This subject is not supported, or is an element not associated with person or household');
return $this->voter->voteOnAttribute($attribute, $subject, $token);
}
}

View File

@@ -61,9 +61,7 @@ class SummaryBudget implements SummaryBudgetInterface
];
}
$personIds = $household->getCurrentPersons()->map(static function (Person $p) {
return $p->getId();
});
$personIds = $household->getCurrentPersons()->map(static function (Person $p) { return $p->getId(); });
$ids = implode(', ', array_fill(0, count($personIds), '?'));
$parameters = [...$personIds, $household->getId()];

View File

@@ -15,14 +15,12 @@ use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Form\CalendarType;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
@@ -32,7 +30,6 @@ use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use DateTimeImmutable;
use Exception;
use http\Exception\UnexpectedValueException;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -66,8 +63,6 @@ class CalendarController extends AbstractController
private SerializerInterface $serializer;
private TranslatableStringHelperInterface $translatableStringHelper;
private UserRepositoryInterface $userRepository;
public function __construct(
@@ -78,7 +73,6 @@ class CalendarController extends AbstractController
PaginatorFactory $paginator,
RemoteCalendarConnectorInterface $remoteCalendarConnector,
SerializerInterface $serializer,
TranslatableStringHelperInterface $translatableStringHelper,
PersonRepository $personRepository,
AccompanyingPeriodRepository $accompanyingPeriodRepository,
UserRepositoryInterface $userRepository
@@ -90,7 +84,6 @@ class CalendarController extends AbstractController
$this->paginator = $paginator;
$this->remoteCalendarConnector = $remoteCalendarConnector;
$this->serializer = $serializer;
$this->translatableStringHelper = $translatableStringHelper;
$this->personRepository = $personRepository;
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
$this->userRepository = $userRepository;
@@ -153,8 +146,6 @@ class CalendarController extends AbstractController
*/
public function editAction(Calendar $entity, Request $request): Response
{
$this->denyAccessUnlessGranted(CalendarVoter::EDIT, $entity);
if (!$this->remoteCalendarConnector->isReady()) {
return $this->remoteCalendarConnector->getMakeReadyResponse($request->getUri());
}
@@ -176,13 +167,8 @@ class CalendarController extends AbstractController
$form = $this->createForm(CalendarType::class, $entity)
->add('save', SubmitType::class);
$form->add('save_and_upload_doc', SubmitType::class);
$templates = $this->docGeneratorTemplateRepository->findByEntity(Calendar::class);
foreach ($templates as $template) {
$form->add('save_and_generate_doc_' . $template->getId(), SubmitType::class, [
'label' => $this->translatableStringHelper->localize($template->getName()),
]);
if (0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class)) {
$form->add('save_and_create_doc', SubmitType::class);
}
$form->handleRequest($request);
@@ -192,18 +178,8 @@ class CalendarController extends AbstractController
$this->addFlash('success', $this->get('translator')->trans('Success : calendar item updated!'));
if ($form->get('save_and_upload_doc')->isClicked()) {
return $this->redirectToRoute('chill_calendar_calendardoc_new', ['id' => $entity->getId()]);
}
foreach ($templates as $template) {
if ($form->get('save_and_generate_doc_' . $template->getId())->isClicked()) {
return $this->redirectToRoute('chill_docgenerator_generate_from_template', [
'entityClassName' => Calendar::class,
'entityId' => $entity->getId(),
'template' => $template->getId(),
]);
}
if ($form->get('save_and_create_doc')->isClicked()) {
return $this->redirectToRoute('chill_calendar_calendardoc_pick_template', ['id' => $entity->getId()]);
}
return new RedirectResponse($redirectRoute);
@@ -221,7 +197,6 @@ class CalendarController extends AbstractController
'accompanyingCourse' => $entity->getAccompanyingPeriod(),
'person' => $entity->getPerson(),
'entity_json' => $entity_array,
'templates' => $templates,
]);
}
@@ -232,8 +207,6 @@ class CalendarController extends AbstractController
*/
public function listActionByCourse(AccompanyingPeriod $accompanyingPeriod): Response
{
$this->denyAccessUnlessGranted(CalendarVoter::SEE, $accompanyingPeriod);
$filterOrder = $this->buildListFilterOrder();
['from' => $from, 'to' => $to] = $filterOrder->getDateRangeData('startDate');
@@ -254,8 +227,7 @@ class CalendarController extends AbstractController
'accompanyingCourse' => $accompanyingPeriod,
'paginator' => $paginator,
'filterOrder' => $filterOrder,
'nbIgnored' => $this->calendarACLAwareRepository->countIgnoredByAccompanyingPeriod($accompanyingPeriod, $from, $to),
'templates' => $this->docGeneratorTemplateRepository->findByEntity(Calendar::class),
'hasDocs' => 0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class),
]);
}
@@ -266,8 +238,6 @@ class CalendarController extends AbstractController
*/
public function listActionByPerson(Person $person): Response
{
$this->denyAccessUnlessGranted(CalendarVoter::SEE, $person);
$filterOrder = $this->buildListFilterOrder();
['from' => $from, 'to' => $to] = $filterOrder->getDateRangeData('startDate');
@@ -288,8 +258,7 @@ class CalendarController extends AbstractController
'person' => $person,
'paginator' => $paginator,
'filterOrder' => $filterOrder,
'nbIgnored' => $this->calendarACLAwareRepository->countIgnoredByPerson($person, $from, $to),
'templates' => $this->docGeneratorTemplateRepository->findByEntity(Calendar::class),
'hasDocs' => 0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class),
]);
}
@@ -333,13 +302,11 @@ class CalendarController extends AbstractController
$entity = new Calendar();
$redirectRoute = '';
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
$view = '@ChillCalendar/Calendar/newByAccompanyingCourse.html.twig';
$entity->setAccompanyingPeriod($accompanyingPeriod);
$redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_period', ['id' => $accompanyingPeriod->getId()]);
} elseif (null !== $person) {
} elseif ($person) {
$view = '@ChillCalendar/Calendar/newByPerson.html.twig';
$entity->setPerson($person)->addPerson($person);
$redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_person', ['id' => $person->getId()]);
@@ -349,18 +316,11 @@ class CalendarController extends AbstractController
$entity->setMainUser($this->userRepository->find($request->query->getInt('mainUser')));
}
$this->denyAccessUnlessGranted(CalendarVoter::CREATE, $entity);
$form = $this->createForm(CalendarType::class, $entity)
->add('save', SubmitType::class);
$templates = $this->docGeneratorTemplateRepository->findByEntity(Calendar::class);
$form->add('save_and_upload_doc', SubmitType::class);
foreach ($templates as $template) {
$form->add('save_and_generate_doc_' . $template->getId(), SubmitType::class, [
'label' => $this->translatableStringHelper->localize($template->getName()),
]);
if (0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class)) {
$form->add('save_and_create_doc', SubmitType::class);
}
$form->handleRequest($request);
@@ -371,25 +331,11 @@ class CalendarController extends AbstractController
$this->addFlash('success', $this->get('translator')->trans('Success : calendar item created!'));
if ($form->get('save_and_upload_doc')->isClicked()) {
return $this->redirectToRoute('chill_calendar_calendardoc_new', ['id' => $entity->getId()]);
if ($form->get('save_and_create_doc')->isClicked()) {
return $this->redirectToRoute('chill_calendar_calendardoc_pick_template', ['id' => $entity->getId()]);
}
foreach ($templates as $template) {
if ($form->get('save_and_generate_doc_' . $template->getId())->isClicked()) {
return $this->redirectToRoute('chill_docgenerator_generate_from_template', [
'entityClassName' => Calendar::class,
'entityId' => $entity->getId(),
'template' => $template->getId(),
]);
}
}
if ('' !== $redirectRoute) {
return new RedirectResponse($redirectRoute);
}
throw new UnexpectedValueException('No person id or accompanying period id was given');
return new RedirectResponse($redirectRoute);
}
if ($form->isSubmitted() && !$form->isValid()) {
@@ -409,7 +355,6 @@ class CalendarController extends AbstractController
'entity' => $entity,
'form' => $form->createView(),
'entity_json' => $entity_array,
'templates' => $templates,
]);
}
@@ -490,8 +435,6 @@ class CalendarController extends AbstractController
*/
public function toActivity(Request $request, Calendar $calendar): RedirectResponse
{
$this->denyAccessUnlessGranted(CalendarVoter::SEE, $calendar);
$personsId = array_map(
static fn (Person $p): int => $p->getId(),
$calendar->getPersons()->toArray()

View File

@@ -12,24 +12,17 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\CalendarBundle\Form\CalendarDocCreateType;
use Chill\CalendarBundle\Form\CalendarDocEditType;
use Chill\CalendarBundle\Security\Voter\CalendarDocVoter;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use RuntimeException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Templating\EngineInterface;
use UnexpectedValueException;
class CalendarDocController
{
@@ -37,201 +30,53 @@ class CalendarDocController
private EngineInterface $engine;
private EntityManagerInterface $entityManager;
private FormFactoryInterface $formFactory;
private Security $security;
private SerializerInterface $serializer;
private UrlGeneratorInterface $urlGenerator;
public function __construct(
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
EngineInterface $engine,
EntityManagerInterface $entityManager,
FormFactoryInterface $formFactory,
Security $security,
UrlGeneratorInterface $urlGenerator
) {
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->engine = $engine;
$this->entityManager = $entityManager;
$this->formFactory = $formFactory;
public function __construct(Security $security, DocGeneratorTemplateRepository $docGeneratorTemplateRepository, UrlGeneratorInterface $urlGenerator, EngineInterface $engine)
{
$this->security = $security;
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->urlGenerator = $urlGenerator;
$this->engine = $engine;
}
/**
* @Route("/{_locale}/calendar/calendar-doc/{id}/new", name="chill_calendar_calendardoc_new")
* @Route("/{_locale}/calendar/docgen/pick/{id}", name="chill_calendar_calendardoc_pick_template")
*/
public function create(Calendar $calendar, Request $request): Response
public function pickTemplate(Calendar $calendar): Response
{
$calendarDoc = (new CalendarDoc($calendar, null))->setCalendar($calendar);
if (!$this->security->isGranted(CalendarDocVoter::EDIT, $calendarDoc)) {
throw new AccessDeniedHttpException();
if (!$this->security->isGranted(CalendarVoter::SEE, $calendar)) {
throw new AccessDeniedException('Not authorized to see this calendar');
}
// set variables
switch ($calendarDoc->getCalendar()->getContext()) {
case 'accompanying_period':
$view = '@ChillCalendar/CalendarDoc/new_accompanying_period.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_period';
$returnParams = ['id' => $calendarDoc->getCalendar()->getAccompanyingPeriod()->getId()];
break;
case 'person':
$view = '@ChillCalendar/CalendarDoc/new_person.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_person';
$returnParams = ['id' => $calendarDoc->getCalendar()->getPerson()->getId()];
break;
default:
throw new UnexpectedValueException('Unsupported context');
if (0 === $number = $this->docGeneratorTemplateRepository->countByEntity(Calendar::class)) {
throw new RuntimeException('should not be redirected to this page if no template');
}
$calendarDocDTO = new CalendarDoc\CalendarDocCreateDTO();
$form = $this->formFactory->create(CalendarDocCreateType::class, $calendarDocDTO);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$calendarDoc->createFromDTO($calendarDocDTO);
$this->entityManager->persist($calendarDoc);
$this->entityManager->flush();
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
if (1 === $number) {
$templates = $this->docGeneratorTemplateRepository->findByEntity(Calendar::class);
return new RedirectResponse(
$this->urlGenerator->generate($returnRoute, $returnParams)
$this->urlGenerator->generate(
'chill_docgenerator_generate_from_template',
[
'template' => $templates[0]->getId(),
'entityClassName' => Calendar::class,
'entityId' => $calendar->getId(),
]
)
);
}
return new Response(
$this->engine->render(
$view,
['calendar_doc' => $calendarDoc, 'form' => $form->createView()]
)
);
}
/**
* @Route("/{_locale}/calendar/calendar-doc/{id}/delete", name="chill_calendar_calendardoc_delete")
*/
public function delete(CalendarDoc $calendarDoc, Request $request): Response
{
if (!$this->security->isGranted(CalendarDocVoter::EDIT, $calendarDoc)) {
throw new AccessDeniedHttpException('Not authorized to delete document');
}
switch ($calendarDoc->getCalendar()->getContext()) {
case 'accompanying_period':
$view = '@ChillCalendar/CalendarDoc/delete_accompanying_period.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_period';
$returnParams = ['id' => $calendarDoc->getCalendar()->getAccompanyingPeriod()->getId()];
break;
case 'person':
$view = '@ChillCalendar/CalendarDoc/delete_person.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_person';
$returnParams = ['id' => $calendarDoc->getCalendar()->getPerson()->getId()];
break;
}
$form = $this->formFactory->createBuilder()
->add('submit', SubmitType::class, [
'label' => 'Delete',
$this->engine->render('@ChillCalendar/CalendarDoc/pick_template.html.twig', [
'calendar' => $calendar,
'accompanyingCourse' => $calendar->getAccompanyingPeriod(),
])
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted()) {
$this->entityManager->remove($calendarDoc);
$this->entityManager->flush();
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
return new RedirectResponse(
$this->urlGenerator->generate($returnRoute, $returnParams)
);
}
return new Response(
$this->engine->render(
$view,
[
'calendar_doc' => $calendarDoc,
'form' => $form->createView(),
]
)
);
}
/**
* @Route("/{_locale}/calendar/calendar-doc/{id}/edit", name="chill_calendar_calendardoc_edit")
*/
public function edit(CalendarDoc $calendarDoc, Request $request): Response
{
if (!$this->security->isGranted(CalendarDocVoter::EDIT, $calendarDoc)) {
throw new AccessDeniedHttpException();
}
// set variables
switch ($calendarDoc->getCalendar()->getContext()) {
case 'accompanying_period':
$view = '@ChillCalendar/CalendarDoc/edit_accompanying_period.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_period';
$returnParams = ['id' => $calendarDoc->getCalendar()->getAccompanyingPeriod()->getId()];
break;
case 'person':
$view = '@ChillCalendar/CalendarDoc/edit_person.html.twig';
$returnRoute = 'chill_calendar_calendar_list_by_person';
$returnParams = ['id' => $calendarDoc->getCalendar()->getPerson()->getId()];
break;
default:
throw new UnexpectedValueException('Unsupported context');
}
$calendarDocEditDTO = new CalendarDoc\CalendarDocEditDTO($calendarDoc);
$form = $this->formFactory->create(CalendarDocEditType::class, $calendarDocEditDTO);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$calendarDoc->editFromDTO($calendarDocEditDTO);
$this->entityManager->flush();
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
return new RedirectResponse(
$this->urlGenerator->generate($returnRoute, $returnParams)
);
}
return new Response(
$this->engine->render(
$view,
['calendar_doc' => $calendarDoc, 'form' => $form->createView()]
)
);
}
}

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\DependencyInjection;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
@@ -53,10 +52,9 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
{
$this->preprendRoutes($container);
$this->prependCruds($container);
$this->prependRoleHierarchy($container);
}
private function prependCruds(ContainerBuilder $container)
protected function prependCruds(ContainerBuilder $container)
{
$container->prependExtensionConfig('chill_main', [
'cruds' => [
@@ -132,18 +130,7 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
]);
}
private function prependRoleHierarchy(ContainerBuilder $container): void
{
$container->prependExtensionConfig('security', [
'role_hierarchy' => [
CalendarVoter::CREATE => [CalendarVoter::SEE],
CalendarVoter::EDIT => [CalendarVoter::SEE],
CalendarVoter::DELETE => [CalendarVoter::SEE],
],
]);
}
private function preprendRoutes(ContainerBuilder $container)
protected function preprendRoutes(ContainerBuilder $container)
{
$container->prependExtensionConfig('chill_main', [
'routing' => [

View File

@@ -18,7 +18,6 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -49,7 +48,7 @@ use function in_array;
* "chill_calendar_calendar": Calendar::class
* })
*/
class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCentersInterface
class Calendar implements TrackCreationInterface, TrackUpdateInterface
{
use RemoteCalendarTrait;
@@ -313,20 +312,6 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
return $this->cancelReason;
}
public function getCenters(): ?iterable
{
switch ($this->getContext()) {
case 'person':
return [$this->getPerson()->getCenter()];
case 'accompanying_period':
return $this->getAccompanyingPeriod()->getCenters();
default:
throw new LogicException('context not supported: ' . $this->getContext());
}
}
public function getComment(): CommentEmbeddable
{
return $this->comment;

View File

@@ -11,8 +11,6 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Entity;
use Chill\CalendarBundle\Entity\CalendarDoc\CalendarDocCreateDTO;
use Chill\CalendarBundle\Entity\CalendarDoc\CalendarDocEditDTO;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
@@ -54,14 +52,14 @@ class CalendarDoc implements TrackCreationInterface, TrackUpdateInterface
* @ORM\ManyToOne(targetEntity=StoredObject::class, cascade={"persist"})
* @ORM\JoinColumn(nullable=false)
*/
private ?StoredObject $storedObject;
private StoredObject $storedObject;
/**
* @ORM\Column(type="boolean", nullable=false, options={"default": false})
*/
private bool $trackDateTimeVersion = false;
public function __construct(Calendar $calendar, ?StoredObject $storedObject)
public function __construct(Calendar $calendar, StoredObject $storedObject)
{
$this->setCalendar($calendar);
@@ -69,22 +67,6 @@ class CalendarDoc implements TrackCreationInterface, TrackUpdateInterface
$this->datetimeVersion = $calendar->getDateTimeVersion();
}
public function createFromDTO(CalendarDocCreateDTO $calendarDocCreateDTO): void
{
$this->storedObject = $calendarDocCreateDTO->doc;
$this->storedObject->setTitle($calendarDocCreateDTO->title);
}
public function editFromDTO(CalendarDocEditDTO $calendarDocEditDTO): void
{
if (null !== $calendarDocEditDTO->doc) {
$calendarDocEditDTO->doc->setTitle($this->getStoredObject()->getTitle());
$this->setStoredObject($calendarDocEditDTO->doc);
}
$this->getStoredObject()->setTitle($calendarDocEditDTO->title);
}
public function getCalendar(): Calendar
{
return $this->calendar;

View File

@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Component\Validator\Constraints as Assert;
class CalendarDocCreateDTO
{
/**
* @Assert\NotNull
* @Assert\Valid
*/
public ?StoredObject $doc = null;
/**
* @Assert\NotBlank
* @Assert\NotNull
*/
public ?string $title = '';
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Component\Validator\Constraints as Assert;
class CalendarDocEditDTO
{
/**
* @Assert\Valid
*/
public ?StoredObject $doc = null;
/**
* @Assert\NotBlank
* @Assert\NotNull
*/
public ?string $title = '';
public function __construct(CalendarDoc $calendarDoc)
{
$this->title = $calendarDoc->getStoredObject()->getTitle();
}
}

View File

@@ -13,21 +13,15 @@ namespace Chill\CalendarBundle\Export\Filter;
use Chill\CalendarBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use DateTime;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class BetweenDatesFilter implements FilterInterface
{
private RollingDateConverterInterface $rollingDateConverter;
public function __construct(RollingDateConverterInterface $rollingDateConverter)
{
$this->rollingDateConverter = $rollingDateConverter;
}
public function addRole(): ?string
{
return null;
@@ -35,21 +29,23 @@ class BetweenDatesFilter implements FilterInterface
public function alterQuery(QueryBuilder $qb, $data)
{
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->andX(
$qb->expr()->gte('cal.startDate', ':dateFrom'),
$qb->expr()->lte('cal.endDate', ':dateTo')
);
$qb->andWhere($clause);
$qb->setParameter(
'dateFrom',
$this->rollingDateConverter->convert($data['date_from'])
);
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->add('where', $where);
$qb->setParameter('dateFrom', $data['date_from'], Types::DATE_MUTABLE);
// modify dateTo so that entire day is also taken into account up until the beginning of the next day.
$qb->setParameter(
'dateTo',
$this->rollingDateConverter->convert($data['date_to'])->modify('+1 day')
);
$qb->setParameter('dateTo', $data['date_to']->modify('+1 day'), Types::DATE_MUTABLE);
}
public function applyOn(): string
@@ -60,19 +56,19 @@ class BetweenDatesFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('date_from', PickRollingDateType::class, [
'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
->add('date_from', ChillDateType::class, [
'data' => new DateTime(),
])
->add('date_to', PickRollingDateType::class, [
'data' => new RollingDate(RollingDate::T_TODAY),
->add('date_to', ChillDateType::class, [
'data' => new DateTime(),
]);
}
public function describeAction($data, $format = 'string'): array
{
return ['Filtered by calendars between %dateFrom% and %dateTo%', [
'%dateFrom%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'),
'%dateTo%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'),
'%dateFrom%' => $data['date_from']->format('d-m-Y'),
'%dateTo%' => $data['date_to']->format('d-m-Y'),
]];
}

View File

@@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\CalendarBundle\Form;
use Chill\CalendarBundle\Entity\CalendarDoc\CalendarDocCreateDTO;
use Chill\DocStoreBundle\Form\StoredObjectType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CalendarDocCreateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'label' => 'chill_calendar.Document title',
'required' => true,
])
->add('doc', StoredObjectType::class, [
'label' => 'chill_calendar.Document object',
'required' => true,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => CalendarDocCreateDTO::class,
]);
}
}

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\CalendarBundle\Form;
use Chill\CalendarBundle\Entity\CalendarDoc\CalendarDocEditDTO;
use Chill\DocStoreBundle\Form\StoredObjectType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CalendarDocEditType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'label' => 'chill_calendar.Document title',
'required' => true,
])
->add('doc', StoredObjectType::class, [
'label' => 'chill_calendar.Document object',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => CalendarDocEditDTO::class,
]);
}
}

View File

@@ -64,38 +64,31 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
return $qb;
}
public function buildQueryByAccompanyingPeriodIgnoredByDates(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): QueryBuilder
{
$qb = $this->em->createQueryBuilder();
$qb->from(Calendar::class, 'c');
$andX = $qb->expr()->andX($qb->expr()->eq('c.accompanyingPeriod', ':period'));
$qb->setParameter('period', $period);
if (null !== $startDate) {
$andX->add($qb->expr()->lt('c.startDate', ':startDate'));
$qb->setParameter('startDate', $startDate);
}
if (null !== $endDate) {
$andX->add($qb->expr()->gt('c.endDate', ':endDate'));
$qb->setParameter('endDate', $endDate);
}
$qb->where($andX);
return $qb;
}
/**
* Base implementation. The list of allowed accompanying period is retrieved "manually" from @see{AccompanyingPeriodACLAwareRepository}.
*/
public function buildQueryByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): QueryBuilder
{
// find the reachable accompanying periods for person
$periods = $this->accompanyingPeriodACLAwareRepository->findByPerson($person, AccompanyingPeriodVoter::SEE);
$qb = $this->em->createQueryBuilder()
->from(Calendar::class, 'c');
$this->addQueryByPersonWithoutDate($qb, $person);
$qb
->where(
$qb->expr()->orX(
// the calendar where the person is the main person:
$qb->expr()->eq('c.person', ':person'),
// when the calendar is in a reachable period, and contains person
$qb->expr()->andX(
$qb->expr()->in('c.accompanyingPeriod', ':periods'),
$qb->expr()->isMemberOf(':person', 'c.persons')
)
)
)
->setParameter('person', $person)
->setParameter('periods', $periods);
// filter by date
if (null !== $startDate) {
@@ -111,30 +104,6 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
return $qb;
}
/**
* Base implementation. The list of allowed accompanying period is retrieved "manually" from @see{AccompanyingPeriodACLAwareRepository}.
*/
public function buildQueryByPersonIgnoredByDates(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): QueryBuilder
{
$qb = $this->em->createQueryBuilder()
->from(Calendar::class, 'c');
$this->addQueryByPersonWithoutDate($qb, $person);
// filter by date
if (null !== $startDate) {
$qb->andWhere($qb->expr()->lt('c.startDate', ':startDate'))
->setParameter('startDate', $startDate);
}
if (null !== $endDate) {
$qb->andWhere($qb->expr()->gt('c.endDate', ':endDate'))
->setParameter('endDate', $endDate);
}
return $qb;
}
public function countByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int
{
$qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('count(c)');
@@ -150,21 +119,6 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
->getSingleScalarResult();
}
public function countIgnoredByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int
{
$qb = $this->buildQueryByAccompanyingPeriodIgnoredByDates($period, $startDate, $endDate)->select('count(c)');
return $qb->getQuery()->getSingleScalarResult();
}
public function countIgnoredByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int
{
return $this->buildQueryByPersonIgnoredByDates($person, $startDate, $endDate)
->select('COUNT(c)')
->getQuery()
->getSingleScalarResult();
}
/**
* @return array|Calendar[]
*/
@@ -206,25 +160,4 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
return $qb->getQuery()->getResult();
}
private function addQueryByPersonWithoutDate(QueryBuilder $qb, Person $person): void
{
// find the reachable accompanying periods for person
$periods = $this->accompanyingPeriodACLAwareRepository->findByPerson($person, AccompanyingPeriodVoter::SEE);
$qb
->where(
$qb->expr()->orX(
// the calendar where the person is the main person:
$qb->expr()->eq('c.person', ':person'),
// when the calendar is in a reachable period, and contains person
$qb->expr()->andX(
$qb->expr()->in('c.accompanyingPeriod', ':periods'),
$qb->expr()->isMemberOf(':person', 'c.persons')
)
)
)
->setParameter('person', $person)
->setParameter('periods', $periods);
}
}

View File

@@ -32,18 +32,6 @@ interface CalendarACLAwareRepositoryInterface
*/
public function countByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int;
/**
* Return the number or calendars associated with an accompanyign period which **does not** match the date conditions.
*/
public function countIgnoredByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int;
/**
* Return the number or calendars associated with a person which **does not** match the date conditions.
*
* See condition on @see{self::findByPerson}.
*/
public function countIgnoredByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int;
/**
* @return array|Calendar[]
*/

View File

@@ -70,7 +70,7 @@
</div>
</div>
<FullCalendar :options="calendarOptions" ref="calendarRef">
<template v-slot:eventContent="arg: EventApi">
<template v-slot:eventContent="arg">
<span :class="eventClasses(arg.event)">
<b v-if="arg.event.extendedProps.is === 'remote'">{{ arg.event.title}}</b>
<b v-else-if="arg.event.extendedProps.is === 'range'">{{ arg.timeText }} - {{ arg.event.extendedProps.locationName }}</b>
@@ -138,6 +138,7 @@ const store = useStore(key);
const {t} = useI18n();
const calendarRef = ref();
const showWeekends = ref(false);
const slotDuration = ref('00:05:00');
const slotMinTime = ref('09:00:00');
@@ -163,10 +164,41 @@ const baseOptions = ref<CalendarOptions>({
eventDrop: onEventDropOrResize,
// when an event si clicked
eventClick: onEventClick,
/* eventContent: function(arg) {
let spanEvent = document.createElement('span')
let rangeEvent = document.createElement('b')
// spanEvent.classList.add(eventClasses(arg.event))
if (arg.event.extendedProps.is === 'remote') {
spanEvent.innerHTML = `<b>${arg.event.title}</b>`
} else if(arg.event.extendedProps.is === 'range') {
spanEvent.innerHTML = `<b>${arg.timeText} - ${arg.event.extendedProps.locationName}</b>
<a class="fa fa-fw fa-times delete"></a>`
} else if(arg.event.extendedProps.is === 'local') {
spanEvent.innerHTML = `<b>${arg.event.title}</b>`
}
let arrayOfDomNodes = [ spanEvent ]
return { domNodes: arrayOfDomNodes }
},*/
selectMirror: false,
editable: true,
customButtons: {
prevWeek: {
text: '<',
click: function() {
navigate('prev')
}
},
nextWeek: {
text: '>',
click: function() {
navigate('next')
}
}
},
headerToolbar: {
left: 'prev,next today',
left: 'prevWeek,nextWeek today',
center: 'title',
right: 'timeGridWeek,timeGridDay'
},
@@ -245,16 +277,43 @@ function onDateSelect(event: DateSelectArg): void {
store.dispatch('calendarRanges/createRange', {start: event.start, end: event.end, location: pickedLocation.value});
}
function navigate(direction: string) {
const viewType = calendarRef.value.getApi().view.type;
const currentStart = store.state.fullCalendar.currentView.start;
const currentEnd = store.state.fullCalendar.currentView.end;
let newDates = {};
if (currentStart != null && currentEnd != null) {
let daysBetween = (currentEnd?.getTime() - currentStart?.getTime())/(1000 * 60 * 60 * 24);
if (daysBetween === 5) {
daysBetween = 7;
}
if (direction === 'prev') {
newDates = {
start: new Date(new Date(currentStart).setDate(currentStart.getDate() - daysBetween)),
end: new Date(new Date(currentEnd).setDate(currentEnd.getDate() - daysBetween))
}
} else if (direction === 'next') {
console.log(daysBetween);
newDates = {
start: new Date(new Date(currentStart).setDate(currentStart.getDate() + daysBetween)),
end: new Date(new Date(currentEnd).setDate(currentEnd.getDate() + daysBetween))
}
console.log(newDates);
}
}
store.dispatch('fullCalendar/setCurrentDatesView', newDates);
calendarRef.value.getApi().changeView('timeGrid', newDates);
}
/**
* When a calendar range is deleted
*/
function onClickDelete(event: EventApi): void {
console.log('onClickDelete', event);
if (event.extendedProps.is !== 'range') {
return;
}
store.dispatch('calendarRanges/deleteRange', event.extendedProps.calendarRangeId);
}

View File

@@ -7,7 +7,7 @@ import App2 from './App2.vue';
import {useI18n} from "vue-i18n";
futureStore().then((store) => {
const i18n = _createI18n(appMessages, false);
const i18n = _createI18n(appMessages, true);
const app = createApp({
template: `<app></app>`,

View File

@@ -31,7 +31,6 @@ export default {
},
actions: {
setCurrentDatesView(ctx: Context, {start, end}: {start: Date|null, end: Date|null}): Promise<null> {
console.log('dispatch setCurrentDatesView', {start, end});
if (ctx.state.currentView.start !== start || ctx.state.currentView.end !== end) {
ctx.commit('setCurrentDatesView', {start, end});

View File

@@ -3,6 +3,11 @@
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
<style lang="css">
--bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;
</style>
<div class="accompanying_course_work-list">
<table class="obj-res-eval my-3">
<thead>
@@ -12,40 +17,27 @@
</thead>
<tbody>
{% for d in calendar.documents %}
{% if is_granted('CHILL_CALENDAR_DOC_SEE', d) %}
<tr>
<td class="eval">
<ul class="eval_title">
<li>
{{ mm.mimeIcon(d.storedObject.type) }}
{{ d.storedObject.title }}
{% if d.dateTimeVersion < d.calendar.dateTimeVersion %}
<span class="badge bg-danger">{{ 'chill_calendar.Document outdated'|trans }}</span>
{% endif %}
<tr>
<td class="eval">
<ul class="eval_title">
<li>
{{ mm.mimeIcon(d.storedObject.type) }}
{{ d.storedObject.title }}
<ul class="record_actions small inline">
{% if chill_document_is_editable(d.storedObject) and is_granted('CHILL_CALENDAR_DOC_EDIT', d) %}
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendardoc_delete', {'id': d.id})}}" class="btn btn-delete"></a>
</li>
<li>
{{ d.storedObject|chill_document_edit_button }}
</li>
{% endif %}
{% if is_granted('CHILL_CALENDAR_DOC_EDIT', d) %}
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendardoc_edit', {'id': d.id})}}" class="btn btn-edit"></a>
</li>
{% endif %}
<li>
{{ m.download_button(d.storedObject, d.storedObject.title) }}
</li>
</ul>
<ul class="record_actions small inline">
{% if chill_document_is_editable(d.storedObject) %}
<li>
{{ d.storedObject|chill_document_edit_button }}
</li>
{% endif %}
<li>
{{ m.download_button(d.storedObject, d.storedObject.title) }}
</li>
</ul>
</td>
</tr>
{% endif %}
</li>
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -1,5 +1,7 @@
{# list used in context of person or accompanyingPeriod #}
{{ filterOrder|chill_render_filter_order_helper }}
{% if calendarItems|length > 0 %}
<div class="flex-table list-records context-accompanyingCourse">
@@ -112,40 +114,13 @@
<div class="item-row">
<ul class="record_actions">
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', calendar) %}
{% if templates|length == 0 %}
<li>
<a class="btn btn-create"
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_new', {'id': calendar.id }) }}">
{{ 'chill_calendar.Add a document'|trans }}
</a>
</li>
{% else %}
<li>
<div class="dropdown">
<button class="btn btn-create dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ 'chill_calendar.Add a document'|trans }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item"
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_new', {'id': calendar.id }) }}">
{{ 'chill_calendar.Upload a document'|trans }}
</a>
</li>
{% for template in templates %}
<li>
<a class="dropdown-item"
href="{{ chill_path_add_return_path('chill_docgenerator_generate_from_template', {'template': template.id, 'entityClassName': 'Chill\\CalendarBundle\\Entity\\Calendar', 'entityId': calendar.id}) }}"
>
{{ template.name|localize_translatable_string }}
</a>
</li>
{% endfor %}
</ul>
</div>
</li>
{% endif %}
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', calendar) and hasDocs %}
<li>
<a class="btn btn-create"
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_pick_template', {'id': calendar.id }) }}">
{{ 'chill_calendar.Add a document'|trans }}
</a>
</li>
{% endif %}
{% if accompanyingCourse is defined and is_granted('CHILL_ACTIVITY_CREATE', accompanyingCourse) and calendar.activity is null %}
<li>

View File

@@ -77,26 +77,9 @@
{{ 'Cancel'|trans|chill_return_path_label }}
</a>
</li>
{% if templates|length == 0 %}
{% if form.save_and_create_doc is defined %}
<li>
{{ form_widget(form.save_and_upload_doc, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'chill_calendar.Create and add a document'|trans }) }}
</li>
{% else %}
<li>
<div class="dropdown">
<button class="btn btn-create dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ 'chill_calendar.Add a document'|trans }}
</button>
<ul class="dropdown-menu">
<li>
{{ form_widget(form.save_and_upload_doc, { 'attr' : { 'class' : 'dropdown-item' }, 'label': 'chill_calendar.Upload a document'|trans }) }}
</li>
{% for template in templates %}
{{ form_widget(form['save_and_generate_doc_' ~ template.id ], {'attr' : { 'class' : 'dropdown-item'}}) }}
{% endfor %}
</ul>
</div>
{{ form_widget(form.save_and_create_doc, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'chill_calendar.Save and add a document'|trans }) }}
</li>
{% endif %}
<li>

View File

@@ -23,15 +23,11 @@
<h1>{{ 'Calendar list' |trans }}</h1>
{{ filterOrder|chill_render_filter_order_helper }}
{% if calendarItems|length == 0 %}
<p class="chill-no-data-statement">
{% if nbIgnored == 0 %}
{{ "There is no calendar items."|trans }}
{% else %}
{{ 'chill_calendar.There are count ignored calendars by date filter'|trans({'nbIgnored': nbIgnored}) }}
{% endif %}
{{ "There is no calendar items."|trans }}
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}"
class="btn btn-create button-small"></a>
</p>
{% else %}
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }}

View File

@@ -22,15 +22,13 @@
<h1>{{ 'Calendar list' |trans }}</h1>
{{ filterOrder|chill_render_filter_order_helper }}
{% if calendarItems|length == 0 %}
<p class="chill-no-data-statement">
{% if nbIgnored == 0 %}
{{ "There is no calendar items."|trans }}
{% else %}
{{ 'chill_calendar.There are count ignored calendars by date filter'|trans({'nbIgnored': nbIgnored}) }}
{% endif %}
{{ "There is no calendar items."|trans }}
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'person_id': person.id}) }}"
class="btn btn-create btn-sm">
{{ 'Create'|trans }}
</a>
</p>
{% else %}
{{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }}

View File

@@ -77,27 +77,10 @@
{{ 'Cancel'|trans|chill_return_path_label }}
</a>
</li>
{% if templates|length == 0 %}
{% if form.save_and_create_doc is defined %}
<li>
{{ form_widget(form.save_and_upload_doc, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'chill_calendar.Create and add a document'|trans }) }}
{{ form_widget(form.save_and_create_doc, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'chill_calendar.Create and add a document'|trans }) }}
</li>
{% else %}
<li>
<div class="dropdown">
<button class="btn btn-create dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ 'chill_calendar.Add a document'|trans }}
</button>
<ul class="dropdown-menu">
<li>
{{ form_widget(form.save_and_upload_doc, { 'attr' : { 'class' : 'dropdown-item' }, 'label': 'chill_calendar.Upload a document'|trans }) }}
</li>
{% for template in templates %}
{{ form_widget(form['save_and_generate_doc_' ~ template.id ], {'attr' : { 'class' : 'dropdown-item'}}) }}
{% endfor %}
</ul>
</div>
</li>
{% endif %}
<li>
{{ form_widget(form.save, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'Create'|trans }) }}

View File

@@ -1,19 +0,0 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Remove a calendar document' |trans }}{% endblock title %}
{% set accompanyingCourse = calendar_doc.calendar.accompanyingPeriod %}
{% set accompanyingCourseId = accompanyingCourse.id %}
{% block content %}
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'chill_calendar.Remove a calendar document'|trans,
'confirm_question' : 'chill_calendar.Are you sure you want to remove the doc?'|trans,
'cancel_route' : 'chill_calendar_calendar_list_by_period',
'cancel_parameters' : { 'id' : accompanyingCourse.id },
'form' : form
} ) }}
{% endblock %}

View File

@@ -1,18 +0,0 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Edit a document' |trans }}{% endblock title %}
{% set person = calendar_doc.calendar.person %}
{% block content %}
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'chill_calendar.Remove a calendar document'|trans,
'confirm_question' : 'chill_calendar.Are you sure you want to remove the doc?'|trans,
'cancel_route' : 'chill_calendar_calendar_list_by_person',
'cancel_parameters' : { 'id' : person.id },
'form' : form
} ) }}
{% endblock %}

View File

@@ -1,40 +0,0 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Edit a document' |trans }}{% endblock title %}
{% set accompanyingCourse = calendar_doc.calendar.accompanyingPeriod %}
{% set accompanyingCourseId = accompanyingCourse.id %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_async_upload') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_async_upload') }}
{% endblock %}
{% block content %}
<h1>{{ 'chill_calendar.Edit a document'|trans }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.doc) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_calendar_calendar_list_by_accompanying_period', {'id': accompanyingCourse.id }) }}">{{ 'Cancel'|trans|chill_return_path_label }}</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@@ -1,39 +0,0 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Edit a document' |trans }}{% endblock title %}
{% set person = calendar_doc.calendar.person %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_async_upload') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_async_upload') }}
{% endblock %}
{% block content %}
<h1>{{ 'chill_calendar.Edit a document'|trans }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.doc) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_calendar_calendar_list_by_person', {'id': person.id }) }}">{{ 'Cancel'|trans|chill_return_path_label }}</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@@ -1,40 +0,0 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Add a document' |trans }}{% endblock title %}
{% set accompanyingCourse = calendar_doc.calendar.accompanyingPeriod %}
{% set accompanyingCourseId = accompanyingCourse.id %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_async_upload') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_async_upload') }}
{% endblock %}
{% block content %}
<h1>{{ 'chill_calendar.Add a document'|trans }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.doc) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_calendar_calendar_list_by_accompanying_period', {'id': accompanyingCourse.id }) }}">{{ 'Cancel'|trans|chill_return_path_label }}</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@@ -1,39 +0,0 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Add a document' |trans }}{% endblock title %}
{% set person = calendar_doc.calendar.person %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_async_upload') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_async_upload') }}
{% endblock %}
{% block content %}
<h1>{{ 'chill_calendar.Add a document'|trans }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.doc) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_calendar_calendar_list_by_person', {'id': person.id }) }}">{{ 'Cancel'|trans|chill_return_path_label }}</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'chill_calendar.Add a document' |trans }}{% endblock title %}
{% set user_id = null %}
{% set accompanying_course_id = accompanyingCourse.id %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_docgen_picktemplate') }}
{% endblock %}
{% block content %}
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-id="{{ calendar.id }}" data-entity-class="{{ 'Chill\\CalendarBundle\\Entity\\Calendar'|e('html_attr') }}"></div>
{% endblock %}

View File

@@ -1 +1 @@
Votre travailleur social {{ calendar.mainUser.label }} vous rencontrera le {{ calendar.startDate|format_date('short', locale='fr') }} à {{ calendar.startDate|format_time('short', locale='fr') }} - {% if calendar.location is not null%}{{ calendar.location.name }}{% endif %}{% if calendar.mainUser.mainLocation is not null and calendar.mainUser.mainLocation.phonenumber1 is not null %} En cas d'indisponibilité, appelez-nous au {{ calendar.mainUser.mainLocation.phonenumber1|chill_format_phonenumber }}.{% endif %}
Votre travailleur social {{ calendar.mainUser.label }} vous rencontrera le {{ calendar.startDate|format_date('short', locale='fr') }} à {{ calendar.startDate|format_time('short', locale='fr') }} - LIEU.{% if calendar.mainUser.mainLocation is not null and calendar.mainUser.mainLocation.phonenumber1 is not null %} En cas d'indisponibilité, appelez-nous au {{ calendar.mainUser.mainLocation.phonenumber1|chill_format_phonenumber }}.{% endif %}

View File

@@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\CalendarBundle\Security\Voter;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
use UnexpectedValueException;
use function in_array;
class CalendarDocVoter extends Voter
{
public const EDIT = 'CHILL_CALENDAR_DOC_EDIT';
public const SEE = 'CHILL_CALENDAR_DOC_SEE';
private const ALL = [
'CHILL_CALENDAR_DOC_EDIT',
'CHILL_CALENDAR_DOC_SEE',
];
private Security $security;
public function __construct(Security $security)
{
$this->security = $security;
}
protected function supports($attribute, $subject): bool
{
return in_array($attribute, self::ALL, true) && $subject instanceof CalendarDoc;
}
/**
* @param CalendarDoc $subject
* @param mixed $attribute
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
switch ($attribute) {
case self::EDIT:
return $this->security->isGranted(CalendarVoter::EDIT, $subject->getCalendar());
case self::SEE:
return $this->security->isGranted(CalendarVoter::SEE, $subject->getCalendar());
default:
throw new UnexpectedValueException('Attribute not supported: ' . $attribute);
}
}
}

View File

@@ -20,14 +20,13 @@ namespace Chill\CalendarBundle\Security\Voter;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use LogicException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
@@ -42,10 +41,6 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
public const SEE = 'CHILL_CALENDAR_CALENDAR_SEE';
private AuthorizationHelperInterface $authorizationHelper;
private CenterResolverManagerInterface $centerResolverManager;
private Security $security;
private VoterHelperInterface $voterHelper;
@@ -57,8 +52,8 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
$this->security = $security;
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(AccompanyingPeriod::class, [self::SEE, self::CREATE])
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(AccompanyingPeriod::class, [self::SEE])
->addCheckFor(Person::class, [self::SEE])
->addCheckFor(Calendar::class, [self::SEE, self::CREATE, self::EDIT, self::DELETE])
->build();
}
@@ -66,9 +61,6 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
public function getRoles(): array
{
return [
self::CREATE,
self::DELETE,
self::EDIT,
self::SEE,
];
}
@@ -80,12 +72,7 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
public function getRolesWithoutScope(): array
{
return [
self::CREATE,
self::DELETE,
self::EDIT,
self::SEE,
];
return [];
}
protected function supports($attribute, $subject): bool
@@ -98,37 +85,48 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
if ($subject instanceof AccompanyingPeriod) {
switch ($attribute) {
case self::SEE:
case self::CREATE:
if ($subject->getStep() === AccompanyingPeriod::STEP_DRAFT) {
return false;
}
// we first check here that the user has read access to the period
if (!$this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject)) {
return false;
}
return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject);
// There is no scope on Calendar, but there are some on accompanying period
// so, to ignore AccompanyingPeriod's scopes, we create a blank Calendar
// linked with an accompanying period.
return $this->voterHelper->voteOnAttribute($attribute, (new Calendar())->setAccompanyingPeriod($subject), $token);
default:
throw new LogicException('subject not implemented');
}
} elseif ($subject instanceof Person) {
switch ($attribute) {
case self::SEE:
case self::CREATE:
return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
return $this->security->isGranted(PersonVoter::SEE, $subject);
default:
throw new LogicException('subject not implemented');
}
} elseif ($subject instanceof Calendar) {
switch ($attribute) {
case self::SEE:
case self::EDIT:
case self::CREATE:
case self::DELETE:
return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
if (null !== $period = $subject->getAccompanyingPeriod()) {
switch ($attribute) {
case self::SEE:
case self::EDIT:
case self::CREATE:
return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $period);
case self::DELETE:
return $this->security->isGranted(AccompanyingPeriodVoter::EDIT, $period);
}
} elseif (null !== $person = $subject->getPerson()) {
switch ($attribute) {
case self::SEE:
case self::EDIT:
case self::CREATE:
return $this->security->isGranted(PersonVoter::SEE, $person);
case self::DELETE:
return $this->security->isGranted(PersonVoter::UPDATE, $person);
}
}
}
throw new LogicException('attribute or not implemented');
throw new LogicException('attribute not implemented');
}
}

View File

@@ -51,13 +51,8 @@ class CalendarForShortMessageProvider
*/
public function getCalendars(DateTimeImmutable $at): iterable
{
$range = $this->rangeGenerator->generateRange($at);
if (null === $range) {
return;
}
['startDate' => $startDate, 'endDate' => $endDate] = $range;
['startDate' => $startDate, 'endDate' => $endDate] = $this->rangeGenerator
->generateRange($at);
$offset = 0;
$batchSize = 10;

View File

@@ -31,14 +31,14 @@ use UnexpectedValueException;
*/
class DefaultRangeGenerator implements RangeGeneratorInterface
{
public function generateRange(\DateTimeImmutable $date): ?array
public function generateRange(\DateTimeImmutable $date): array
{
$onMidnight = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date->format('Y-m-d') . ' 00:00:00');
switch ($dow = (int) $onMidnight->format('w')) {
case 6: // Saturday
case 0: // Sunday
return null;
return ['startDate' => null, 'endDate' => null];
case 1: // Monday
// send for Tuesday and Wednesday

View File

@@ -23,7 +23,7 @@ use DateTimeImmutable;
interface RangeGeneratorInterface
{
/**
* @return ?array{startDate: DateTimeImmutable, endDate: DateTimeImmutable} when return is null, then no ShortMessage must be send
* @return array<startDate: \DateTimeImmutable, endDate: \DateTimeImmutable>
*/
public function generateRange(DateTimeImmutable $date): ?array;
public function generateRange(DateTimeImmutable $date): array;
}

View File

@@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\CalendarBundle\Tests\Entity;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
final class CalendarDocTest extends TestCase
{
public function testCreateEditFromDTO(): void
{
$doc = new CalendarDoc(new Calendar(), null);
$create = new CalendarDoc\CalendarDocCreateDTO();
$create->title = 'tagada';
$create->doc = $obj1 = new StoredObject();
$doc->createFromDTO($create);
$this->assertSame($obj1, $doc->getStoredObject());
$this->assertEquals('tagada', $doc->getStoredObject()->getTitle());
$edit = new CalendarDoc\CalendarDocEditDTO($doc);
$edit->title = 'tsointsoin';
$doc->editFromDTO($edit);
$this->assertSame($obj1, $doc->getStoredObject());
$this->assertEquals('tsointsoin', $doc->getStoredObject()->getTitle());
$edit2 = new CalendarDoc\CalendarDocEditDTO($doc);
$edit2->doc = $obj2 = new StoredObject();
$doc->editFromDTO($edit2);
$this->assertSame($obj2, $doc->getStoredObject());
$this->assertEquals('tsointsoin', $doc->getStoredObject()->getTitle());
$edit3 = new CalendarDoc\CalendarDocEditDTO($doc);
$edit3->doc = $obj3 = new StoredObject();
$edit3->title = 'tagada';
$doc->editFromDTO($edit3);
$this->assertSame($obj3, $doc->getStoredObject());
$this->assertEquals('tagada', $doc->getStoredObject()->getTitle());
}
}

View File

@@ -22,7 +22,6 @@ use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Repository\CalendarRepository;
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator;
use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
@@ -38,32 +37,6 @@ final class CalendarForShortMessageProviderTest extends TestCase
{
use ProphecyTrait;
public function testGenerateRangeIsNull()
{
$calendarRepository = $this->prophesize(CalendarRepository::class);
$calendarRepository->findByNotificationAvailable(
Argument::type(DateTimeImmutable::class),
Argument::type(DateTimeImmutable::class),
Argument::type('int'),
Argument::exact(0)
)->shouldBeCalledTimes(0);
$rangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
$rangeGenerator->generateRange(Argument::type(DateTimeImmutable::class))->willReturn(null);
$em = $this->prophesize(EntityManagerInterface::class);
$em->clear()->shouldNotBeCalled();
$provider = new CalendarForShortMessageProvider(
$calendarRepository->reveal(),
$em->reveal(),
$rangeGenerator->reveal()
);
$calendars = iterator_to_array($provider->getCalendars(new DateTimeImmutable('now')));
$this->assertEquals(0, count($calendars));
}
public function testGetCalendars()
{
$calendarRepository = $this->prophesize(CalendarRepository::class);

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Calendar;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20221125144205 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException();
}
public function getDescription(): string
{
return 'Calendar: remove association between scope and calendar';
}
public function up(Schema $schema): void
{
$this->addSql(
sprintf(
'UPDATE role_scopes SET scope_id=NULL WHERE role IN (\'%s\', \'%s\', \'%s\', \'%s\')',
'CHILL_CALENDAR_CALENDAR_CREATE',
'CHILL_CALENDAR_CALENDAR_DELETE',
'CHILL_CALENDAR_CALENDAR_EDIT',
'CHILL_CALENDAR_CALENDAR_SEE'
)
);
}
}

View File

@@ -1,8 +0,0 @@
chill_calendar:
There are count ignored calendars by date filter: >-
{nbIgnored, plural,
=0 {Il n'y a aucun rendez-vous ignoré par le filtre de date.}
one {Il y a un rendez-vous ignoré par le filtre de date. Modifiez le filtre de date pour le voir apparaitre.}
few {# rendez-vous sont ignorés par le filtre de date. Modifiez le filtre de date pour les voir apparaitre.}
other {# rendez-vous sont ignorés par le filtre de date. Modifiez le filtre de date pour les voir apparaitre.}
}

View File

@@ -55,14 +55,6 @@ chill_calendar:
Create and add a document: Créer et ajouter un document
Save and add a document: Enregistrer et ajouter un document
Create for me: Créer un rendez-vous pour moi-même
Edit a document: Modifier un document
Document title: Titre
Document object: Document
Add a document from template: Ajouter un document depuis un gabarit
Upload a document: Téléverser un document
Remove a calendar document: Supprimer un document d'un rendez-vous
Are you sure you want to remove the doc?: Êtes-vous sûr·e de vouloir supprimer le document associé ?
Document outdated: La date et l'heure du rendez-vous ont été modifiés après la création du document
remote_ms_graph:
@@ -120,7 +112,6 @@ Job: Métier
Location type: Type de localisation
Location: Lieu de rendez-vous
by month and year: Par mois et année
is urgent: Urgent
is not urgent: Pas urgent
has calendar range: Dans une plage de disponibilité?
@@ -139,9 +130,3 @@ docgen:
Destinee: Destinataire
None: Aucun choix
title of the generated document: Titre du document généré
CHILL_CALENDAR_CALENDAR_CREATE: Créer les rendez-vous
CHILL_CALENDAR_CALENDAR_EDIT: Modifier les rendez-vous
CHILL_CALENDAR_CALENDAR_DELETE: Supprimer les rendez-vous
CHILL_CALENDAR_CALENDAR_SEE: Voir les rendez-vous

View File

@@ -1,72 +1,64 @@
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
{% if document is null %}
<div class="alert alert-warning">
{{ 'workflow.Document deleted'|trans }}
</div>
{% else %}
<div class="flex-table accompanying_course_work-list">
<div class="item-bloc document-item bg-chill-llight-gray">
<div class="row justify-content-center my-4">
<div class="col-2">
<i class="fa fa-4x fa-file-text-o text-success"></i>
</div>
<div class="col-8">
<h3>{{ document.title }}</h3>
<div class="flex-table accompanying_course_work-list">
<div class="item-bloc document-item bg-chill-llight-gray">
<div class="row justify-content-center my-4">
<div class="col-2">
<i class="fa fa-4x fa-file-text-o text-success"></i>
</div>
<div class="col-8">
<h3>{{ document.title }}</h3>
{{ mm.mimeIcon(document.object.type) }}
{{ mm.mimeIcon(document.object.type) }}
{% if document.description is not empty %}
<blockquote class="chill-user-quote mt-4">
{{ document.description }}
</blockquote>
{% endif %}
{% if document.description is not empty %}
<blockquote class="chill-user-quote mt-4">
{{ document.description }}
</blockquote>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% set freezed = false %}
{% for step in entity_workflow.stepsChained %}
{% if loop.last %}
{% if step.previous is not null and step.previous.freezeAfter == true %}
{% set freezed = true %}
{% set freezed = false %}
{% for step in entity_workflow.stepsChained %}
{% if loop.last %}
{% if step.previous is not null and step.previous.freezeAfter == true %}
{% set freezed = true %}
{% endif %}
{% endif %}
{% endfor %}
{% if display_action is defined and display_action == true %}
<ul class="record_actions">
{% if document.course != null and is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', document.course) %}
<li>
<a href="{{ path('chill_person_accompanying_course_index', {'accompanying_period_id': document.course.id}) }}" class="btn btn-show change-icon">
<i class="fa fa-random"></i> {{ 'Course number'|trans }} {{ document.course.id }}
</a>
</li>
{% endif %}
<li>
{{ m.download_button(document.object, document.title) }}
</li>
<li>
{% if chill_document_is_editable(document.object) %}
{% if not freezed %}
{{ document.object|chill_document_edit_button({'title': document.title|e('html') }) }}
{% else %}
<a class="btn btn-wopilink disabled" href="#" title="{{ 'workflow.freezed document'|trans }}">
{{ 'Update document'|trans }}
</a>
{% endif %}
{% endif %}
{% endfor %}
{% if display_action is defined and display_action == true %}
<ul class="record_actions">
{% if document.course != null and is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', document.course) %}
<li>
<a href="{{ path('chill_person_accompanying_course_index', {'accompanying_period_id': document.course.id}) }}" class="btn btn-show change-icon">
<i class="fa fa-random"></i> {{ 'Course number'|trans }} {{ document.course.id }}
</a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE', document) and document.course != null %}
<li>
{{ m.download_button(document.object, document.title) }}
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': document.course.id, 'id': document.id}) }}" class="btn btn-show"></a>
</li>
{% if chill_document_is_editable(document.object) %}
{% if not freezed %}
<li>
{{ document.object|chill_document_edit_button({'title': document.title|e('html') }) }}
</li>
{% else %}
<li>
<a class="btn btn-wopilink disabled" href="#" title="{{ 'workflow.freezed document'|trans }}">
{{ 'Update document'|trans }}
</a>
</li>
{% endif %}
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE', document) and document.course != null %}
<li>
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': document.course.id, 'id': document.id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
</ul>
{% endif %}
{% endif %}
</li>
</ul>
{% endif %}

View File

@@ -60,7 +60,7 @@ final class StoredObjectManager implements StoredObjectManagerInterface
$this
->tempUrlGenerator
->generate(
Request::METHOD_HEAD,
Request::METHOD_PUT,
$document->getFilename()
)
->url

View File

@@ -65,10 +65,6 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
{
$doc = $this->getRelatedEntity($entityWorkflow);
if (null === $doc) {
return $this->translator->trans('workflow.Document deleted');
}
return $this->translator->trans('workflow.Document (n°%doc%)', ['%doc%' => $entityWorkflow->getRelatedEntityId()])
. ' - ' . $doc->getTitle();
}

View File

@@ -63,6 +63,3 @@ Create new DocumentCategory: Créer une nouvelle catégorie de document
# WOPI EDIT
online_edit_document: Éditer en ligne
workflow:
Document deleted: Document supprimé

View File

@@ -151,7 +151,7 @@ class PickEventType extends AbstractType
} else {
$centers = $this->authorizationHelper->getReachableCenters(
$this->user,
(string) $options['role']->getRole()
(string) $options['role']
);
}

View File

@@ -5,7 +5,6 @@
{% block title 'Delete event'|trans %}
{% block event_content %}
<div class="col-10">
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
@@ -16,6 +15,6 @@
'form' : delete_form
}
) }}
</div>
{% endblock %}

View File

@@ -3,7 +3,6 @@
{% block title 'Event edit'|trans %}
{% block event_content -%}
<div class="col-10">
<h1>{{ 'Event edit'|trans }}</h1>
{{ form_start(edit_form) }}
@@ -29,8 +28,6 @@
{{ form_widget(edit_form.submit, { 'attr' : { 'class' : 'btn btn-update' } }) }}
</li>
</ul>
{{ form_end(edit_form) }}
</div>
{% endblock %}

View File

@@ -6,42 +6,42 @@
<p>{{ 'Results %start%-%end% of %total%'|trans({ '%start%' : start, '%end%': start + events|length, '%total%' : total } ) }}</p>
<table class="table events">
<table class="events">
<thead>
<tr>
<th class="chill-red">{{ 'Name'|trans }}</th>
<th class="chill-green">{{ 'Date'|trans }}</th>
<th class="chill-orange">{{ 'Event type'|trans }}</th>
<th>&nbsp;</th>
</tr>
<tr>
<th class="chill-red">{{ 'Name'|trans }}</th>
<th class="chill-green">{{ 'Date'|trans }}</th>
<th class="chill-orange">{{ 'Event type'|trans }}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td>{{ event.name }}</td>
<td>{{ event.date|format_date('long') }}</td>
<td>{{ event.type.name|localize_translatable_string }}</td>
<td>
<ul class="record_actions">
<li>
{# {% if is_granted('CHILL_EVENT_SEE_DETAILS', event) %} #}
<a href="{{ path('chill_event__event_show', { 'event_id' : event.id } ) }}" class="btn btn-dark">
{{ 'See'|trans }}
</a>
{# {% endif %} #}
{% if is_granted('CHILL_EVENT_UPDATE', event) %}
{% for event in events %}
<tr>
<td>{{ event.name }}</td>
<td>{{ event.date|format_date('long') }}</td>
<td>{{ event.type.name|localize_translatable_string }}</td>
<td>
<ul class="record_actions">
<li>
{# {% if is_granted('CHILL_EVENT_SEE_DETAILS', event) %} #}
<a href="{{ path('chill_event__event_show', { 'event_id' : event.id } ) }}" class="btn btn-dark">
{{ 'See'|trans }}
</a>
{# {% endif %} #}
{% if is_granted('CHILL_EVENT_UPDATE', event) %}
<a href="{{ path('chill_event__event_edit', { 'event_id' : event.id } ) }}" class="btn btn-update">
{{ 'Edit'|trans }}
</a>
{% endif %}
</li>
</ul>
</td>
</tr>
{% endfor %}
{% endif %}
</li>
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<ul class="record_actions">
@@ -49,18 +49,17 @@
<a href="{{ path('chill_event__event_new_pickcenter') }}" class="btn btn-create" >
{{ 'New event'|trans }}
</a>
</li>
</li>
{% if preview == true and events|length < total %}
<li>
<a href="{{ path('chill_main_search', { "name": search_name, "q" : pattern }) }}" class="btn btn-misc">
{{ 'See all results'|trans }}
</a>
</li>
<li>
<a href="{{ path('chill_main_search', { "name": search_name, "q" : pattern }) }}" class="btn btn-next">
{{ 'See all results'|trans }}
</a>
</li>
{% endif %}
</ul>
{% if preview == false %}
{{ chill_pagination(paginator) }}
{{ chill_pagination(paginator) }}
{% endif %}

View File

@@ -44,59 +44,59 @@
<td>{{ participation.role.name|localize_translatable_string }}</td>
<td>{{ participation.status.name|localize_translatable_string }}</td>
<td>
<div class="btn-group" role="group" aria-label="Button group actions">
<ul class="list-inline">
{% set currentPath = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
{% set returnLabel = 'Back to %person% events'|trans({ '%person%' : currentPerson } ) %}
{% if is_granted('CHILL_EVENT_SEE_DETAILS', participation.event) %}
<li class="list-inline-item">
<a href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel } ) }}"
class="btn btn-primary btn-sm" title="{{ 'See details of the event'|trans }}">
<i class="fa fa-fw fa-eye"></i>
</a>
class="btn btn-primary btn-sm" title="{{ 'See details of the event'|trans }}"><i class="fa fa-fw fa-eye"></i></a>
</li>
{% endif %}
{% if is_granted('CHILL_EVENT_UPDATE', participation.event)
and is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
<div class="btn-group" role="group">
<button class="btn btn-sm btn-warning dropdown-toggle" type="button" id="dropdownEdit" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-pencil"></i>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownEdit">
<li>
<li class="list-inline-item">
<div class="btn dropdown-toggle">
<a href="" class="btn btn-warning btn-sm"><i class="fa fa-fw fa-pencil"></i></a>
<div class="dropdown-menu">
<a href="{{ path('chill_event__event_edit', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
class="dropdown-item">
class="btn btn-warning btn-sm">
{{ 'Edit the event'|trans }}
</a>
</li>
<li>
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
class="dropdown-item">
class="btn btn-warning btn-sm">
{{ 'Edit the participation'|trans }}
</a>
</li>
</ul>
</div>
</div>
</div>
</li>
{% else %}
<li class="list-inline-item">
{% if is_granted('CHILL_EVENT_UPDATE', participation.event) %}
<a href="{{ path('chill_event__event_edit', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
class="btn btn-warning btn-sm">
{{ 'Edit the event'|trans }}
</a>
<a href="{{ path('chill_event__event_edit', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
class="btn btn-warning btn-sm">
{{ 'Edit the event'|trans }}
</a>
{% endif %}
{% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
class="btn btn-warning btn-sm">
{{ 'Edit the participation'|trans }}
</a>
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
class="btn btn-warning btn-sm">
{{ 'Edit the participation'|trans }}
</a>
{% endif %}
</li>
{% endif %}
</div>
</ul>
</td>
</tr>
{% endfor %}
@@ -108,17 +108,31 @@
{{ chill_pagination(paginator) }}
{% endif %}
<div class="input-group mt-5">
<div class="input-group mb-3">
{{ form_start(form_add_event_participation_by_person) }}
{{ form_widget(form_add_event_participation_by_person.event_id, { 'attr' : {
'class' : 'custom-select',
'style': 'min-width: 15em; max-width: 18em; display: inline-block;'
}}) }}
<div class="input-group-append">
{{ form_widget(form_add_event_participation_by_person.submit, { 'attr' : { 'class' : 'btn btn-sm btn-save' } } ) }}
{#
<input type="text" class="form-control" placeholder="Recipient's username" aria-label="Recipient's username" aria-describedby="button-addon2">
#}
{{ form_widget(form_add_event_participation_by_person.event_id, { 'attr' : { 'class' : 'form-control' } } ) }}
<div class="input-group-append input-group-btn">
{#
<button class="btn btn-outline-secondary" type="button" id="button-addon2">Button</button>
#}
{{ form_widget(form_add_event_participation_by_person.submit, { 'attr' : { 'class' : 'btn btn-success' } } ) }}
</div>
{{ form_rest(form_add_event_participation_by_person) }}
{{ form_end(form_add_event_participation_by_person) }}
</div>
{#
{{ form(form_add_event_participation_by_person) }}
#}
<div class="input-group mb-3">
<input class="form-control" placeholder="Recipient's username">
<div class="input-group-append">
<button class="btn btn-success">Button</button>
</div>
</div>
{% endblock %}

View File

@@ -3,7 +3,6 @@
{% block title 'Event creation'|trans %}
{% block event_content -%}
<div class="col-10">
<h1>{{ 'Event creation'|trans }}</h1>
{{ form_start(form) }}
@@ -27,5 +26,4 @@
</ul>
{{ form_end(form) }}
</div>
{% endblock %}

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