Compare commits

..

6 Commits

310 changed files with 2061 additions and 11209 deletions

2
.gitignore vendored
View File

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

View File

@@ -14,7 +14,7 @@
"champs-libres/wopi-lib": "dev-master#8615f4a45a39fc2b6a98765ea835fcfd39618787",
"doctrine/doctrine-bundle": "^2.1",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "2.13.5",
"doctrine/orm": "^2.7",
"erusev/parsedown": "^1.7",
"graylog2/gelf-php": "^1.5",
"knplabs/knp-menu-bundle": "^3.0",

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,19 +36,6 @@ 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
=======================

View File

@@ -6,7 +6,7 @@ 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 |
@@ -36,7 +36,6 @@ These are alias conventions :
| | 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 |

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

@@ -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
@@ -277,10 +277,6 @@ 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é
Activity user: Utilisateur lié à l'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,7 +16,7 @@ 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"

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,27 +331,13 @@ 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');
}
if ($form->isSubmitted() && !$form->isValid()) {
$this->addFlash('error', $this->get('translator')->trans('This form contains errors'));
}
@@ -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)
);
}
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',
])
->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,
$this->urlGenerator->generate(
'chill_docgenerator_generate_from_template',
[
'calendar_doc' => $calendarDoc,
'form' => $form->createView(),
'template' => $templates[0]->getId(),
'entityClassName' => Calendar::class,
'entityId' => $calendar->getId(),
]
)
);
}
/**
* @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()]
)
$this->engine->render('@ChillCalendar/CalendarDoc/pick_template.html.twig', [
'calendar' => $calendar,
'accompanyingCourse' => $calendar->getAccompanyingPeriod(),
])
);
}
}

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,31 +17,19 @@
</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 %}
<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>
{% if chill_document_is_editable(d.storedObject) %}
<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>
@@ -45,7 +38,6 @@
</ul>
</td>
</tr>
{% endif %}
{% 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 %}
{% 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_new', {'id': calendar.id }) }}">
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_pick_template', {'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 %}
{% 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 %}
<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 %}
<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,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.Create and add a document'|trans }) }}
</li>
{% endif %}
<li>

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) {
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->voterHelper->voteOnAttribute($attribute, $subject, $token);
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,12 +1,7 @@
{% 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="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">
@@ -26,18 +21,18 @@
</div>
</div>
</div>
</div>
</div>
{% set freezed = false %}
{% for step in entity_workflow.stepsChained %}
{% 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 %}
{% endfor %}
{% if display_action is defined and display_action == true %}
{% 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>
@@ -49,17 +44,14 @@
<li>
{{ m.download_button(document.object, document.title) }}
</li>
<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 %}
@@ -67,6 +59,6 @@
<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 %}
</li>
</ul>
{% endif %}
{% endif %}

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) }}
@@ -31,6 +30,4 @@
</ul>
{{ form_end(edit_form) }}
</div>
{% endblock %}

View File

@@ -6,7 +6,7 @@
<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>
@@ -52,7 +52,7 @@
</li>
{% if preview == true and events|length < total %}
<li>
<a href="{{ path('chill_main_search', { "name": search_name, "q" : pattern }) }}" class="btn btn-misc">
<a href="{{ path('chill_main_search', { "name": search_name, "q" : pattern }) }}" class="btn btn-next">
{{ 'See all results'|trans }}
</a>
</li>
@@ -61,6 +61,5 @@
</ul>
{% if preview == false %}
{{ chill_pagination(paginator) }}
{{ chill_pagination(paginator) }}
{% endif %}

View File

@@ -44,42 +44,41 @@
<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>
</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">
@@ -92,11 +91,12 @@
{{ '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 %}

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) }}
@@ -23,5 +22,5 @@
</ul>
{{ form_end(form) }}
</div>
{% endblock %}

View File

@@ -5,10 +5,9 @@
{% import 'ChillPersonBundle:Person:macro.html.twig' as person_macro %}
{% block event_content -%}
<div class="col-10">
<h1>{{ 'Details of an event'|trans }}</h1>
<table class="table record_properties">
<table class="record_properties">
<tbody>
<tr>
<th>{{ 'Name'|trans }}</th>
@@ -70,7 +69,7 @@
<p>{% transchoice count %}%count% participations to this event{% endtranschoice %}</p>
{% if count > 0 %}
<table class="table">
<table>
<thead>
<tr>
<th>{{ 'Person'|trans }}</th>
@@ -118,7 +117,7 @@
{% endif %}
</ul>
<div class="row" style="margin-bottom: 10em;">
<div style="margin-bottom: 10em;">
<div class="col-8">
{{ form_start(form_add_participation_by_person) }}
<div class="input-group">
@@ -151,5 +150,5 @@
<div class="post_show">
{{ chill_delegated_block('block_footer_show', { 'event': event }) }}
</div>
</div>
{% endblock %}

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