Merge branch 'refs/heads/master' into ticket-app-master

# Conflicts:
#	composer.json
#	config/bundles.php
#	config/packages/doctrine_migrations_chill.yaml
#	package.json
#	src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUserGroup.php
#	src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php
#	src/Bundle/ChillMainBundle/Entity/UserGroup.php
#	src/Bundle/ChillMainBundle/Resources/public/chill/js/date.ts
#	src/Bundle/ChillMainBundle/Resources/public/lib/download-report/download-report.js
#	src/Bundle/ChillMainBundle/Resources/public/module/ckeditor5/editor_config.ts
#	src/Bundle/ChillMainBundle/Resources/public/module/ckeditor5/index.ts
#	src/Bundle/ChillMainBundle/Resources/public/page/export/download-export.js
#	src/Bundle/ChillMainBundle/Resources/public/types.ts
#	src/Bundle/ChillMainBundle/Resources/views/Dev/dev.assets.html.twig
#	src/Bundle/ChillMainBundle/Templating/Entity/UserGroupRender.php
#	src/Bundle/ChillMainBundle/chill.api.specs.yaml
#	src/Bundle/ChillMainBundle/chill.webpack.config.js
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Comment.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/WriteComment.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Household.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/MemberDetails.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/PersonComment.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonText.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/_js/i18n.ts
#	tests/app/config/bootstrap.php
This commit is contained in:
2025-05-27 09:37:04 +02:00
1735 changed files with 82701 additions and 27873 deletions

View File

@@ -63,7 +63,6 @@ abstract class AbstractCRUDController extends AbstractController
parent::getSubscribedServices(),
[
'chill_main.paginator_factory' => PaginatorFactory::class,
ManagerRegistry::class => ManagerRegistry::class,
'translator' => TranslatorInterface::class,
AuthorizationHelper::class => AuthorizationHelper::class,
EventDispatcherInterface::class => EventDispatcherInterface::class,
@@ -213,7 +212,7 @@ abstract class AbstractCRUDController extends AbstractController
protected function getManagerRegistry(): ManagerRegistry
{
return $this->container->get(ManagerRegistry::class);
return $this->container->get('doctrine');
}
/**
@@ -226,7 +225,7 @@ abstract class AbstractCRUDController extends AbstractController
protected function getValidator(): ValidatorInterface
{
return $this->get('validator');
return $this->container->get('validator');
}
/**

View File

@@ -580,7 +580,7 @@ class CRUDController extends AbstractController
string $action,
mixed $entity,
Request $request,
array $defaultTemplateParameters = []
array $defaultTemplateParameters = [],
) {
return $defaultTemplateParameters;
}
@@ -685,7 +685,7 @@ class CRUDController extends AbstractController
Request $request,
int $totalItems,
PaginatorInterface $paginator,
?FilterOrderHelper $filterOrder = null
?FilterOrderHelper $filterOrder = null,
) {
$query = $this->queryEntities($action, $request, $paginator, $filterOrder);

View File

@@ -18,7 +18,6 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\ExportsCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\ShortMessageCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
@@ -73,6 +72,5 @@ class ChillMainBundle extends Bundle
$container->addCompilerPass(new MenuCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ACLFlagsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new CRUDControllerCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ShortMessageCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
}
}

View File

@@ -57,7 +57,7 @@ class ChillImportUsersCommand extends Command
protected LoggerInterface $logger,
protected \Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface $passwordEncoder,
protected ValidatorInterface $validator,
protected UserRepository $userRepository
protected UserRepository $userRepository,
) {
parent::__construct('chill:main:import-users');
}

View File

@@ -69,7 +69,7 @@ class ChillUserSendRenewPasswordCodeCommand extends Command
LoggerInterface $logger,
EntityManagerInterface $em,
RecoverPasswordHelper $recoverPasswordHelper,
EventDispatcherInterface $eventDispatcher
EventDispatcherInterface $eventDispatcher,
) {
$this->logger = $logger;
$this->em = $em;

View File

@@ -22,7 +22,7 @@ class ExecuteCronJobCommand extends Command
protected static $defaultDescription = 'Execute the cronjob(s) given as argument, or one cronjob scheduled by system.';
public function __construct(
private readonly CronManagerInterface $cronManager
private readonly CronManagerInterface $cronManager,
) {
parent::__construct('chill:cron-job:execute');
}

View File

@@ -16,6 +16,7 @@ use Chill\MainBundle\Service\Import\PostalCodeBEFromBestAddress;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class LoadAddressesBEFromBestAddressCommand extends Command
@@ -24,7 +25,7 @@ class LoadAddressesBEFromBestAddressCommand extends Command
public function __construct(
private readonly AddressReferenceBEFromBestAddress $addressImporter,
private readonly PostalCodeBEFromBestAddress $postalCodeBEFromBestAddressImporter
private readonly PostalCodeBEFromBestAddress $postalCodeBEFromBestAddressImporter,
) {
parent::__construct();
}
@@ -34,14 +35,19 @@ class LoadAddressesBEFromBestAddressCommand extends Command
$this
->setName('chill:main:address-ref-from-best-addresses')
->addArgument('lang', InputArgument::REQUIRED, "Language code, for example 'fr'")
->addArgument('list', InputArgument::IS_ARRAY, "The list to add, for example 'full', or 'extract' (dev) or '1xxx' (brussel CP)");
->addArgument('list', InputArgument::IS_ARRAY, "The list to add, for example 'full', or 'extract' (dev) or '1xxx' (brussel CP)")
->addOption('send-report-email', 's', InputOption::VALUE_REQUIRED, 'Email address where a list of unimported addresses can be send');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->postalCodeBEFromBestAddressImporter->import();
$this->addressImporter->import($input->getArgument('lang'), $input->getArgument('list'));
$this->addressImporter->import(
$input->getArgument('lang'),
$input->getArgument('list'),
$input->hasOption('send-report-email') ? $input->getOption('send-report-email') : null
);
return Command::SUCCESS;
}

View File

@@ -0,0 +1,48 @@
<?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\MainBundle\Command;
use Chill\MainBundle\Service\Import\AddressReferenceFromBAN;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class LoadAddressesFRFromBANCommand extends Command
{
protected static $defaultDescription = 'Import FR addresses from BAN (see https://adresses.data.gouv.fr';
public function __construct(private readonly AddressReferenceFromBAN $addressReferenceFromBAN)
{
parent::__construct();
}
protected function configure()
{
$this->setName('chill:main:address-ref-from-ban')
->addArgument('departementNo', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'a list of departement numbers')
->addOption('send-report-email', 's', InputOption::VALUE_REQUIRED, 'Email address where a list of unimported addresses can be send');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
dump(__METHOD__);
foreach ($input->getArgument('departementNo') as $departementNo) {
$output->writeln('Import addresses for '.$departementNo);
$this->addressReferenceFromBAN->import($departementNo, $input->hasOption('send-report-email') ? $input->getOption('send-report-email') : null);
}
return Command::SUCCESS;
}
}

View File

@@ -15,6 +15,7 @@ use Chill\MainBundle\Service\Import\AddressReferenceFromBano;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class LoadAddressesFRFromBANOCommand extends Command
@@ -29,7 +30,8 @@ class LoadAddressesFRFromBANOCommand extends Command
protected function configure()
{
$this->setName('chill:main:address-ref-from-bano')
->addArgument('departementNo', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'a list of departement numbers');
->addArgument('departementNo', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'a list of departement numbers')
->addOption('send-report-email', 's', InputOption::VALUE_REQUIRED, 'Email address where a list of unimported addresses can be send');
}
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -37,7 +39,7 @@ class LoadAddressesFRFromBANOCommand extends Command
foreach ($input->getArgument('departementNo') as $departementNo) {
$output->writeln('Import addresses for '.$departementNo);
$this->addressReferenceFromBano->import($departementNo);
$this->addressReferenceFromBano->import($departementNo, $input->hasOption('send-report-email') ? $input->getOption('send-report-email') : null);
}
return Command::SUCCESS;

View File

@@ -0,0 +1,45 @@
<?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\MainBundle\Command;
use Chill\MainBundle\Service\Import\AddressReferenceLU;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class LoadAddressesLUFromBDAddressCommand extends Command
{
protected static $defaultDescription = 'Import LUX addresses from BD addresses (see https://data.public.lu/fr/datasets/adresses-georeferencees-bd-adresses/)';
public function __construct(
private readonly AddressReferenceLU $addressImporter,
) {
parent::__construct();
}
protected function configure()
{
$this
->setName('chill:main:address-ref-lux')
->addOption('send-report-email', 's', InputOption::VALUE_REQUIRED, 'Email address where a list of unimported addresses can be send');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->addressImporter->import(
$input->hasOption('send-report-email') ? $input->getOption('send-report-email') : null,
);
return Command::SUCCESS;
}
}

View File

@@ -632,7 +632,7 @@ class ExportController extends AbstractController
}
}
private function rebuildRawData(string $key): array
private function rebuildRawData(?string $key): array
{
if (null === $key) {
throw $this->createNotFoundException('key does not exists');

View File

@@ -0,0 +1,32 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class GenderApiController extends ApiController
{
protected function customizeQuery(string $action, Request $request, $query): void
{
$query
->andWhere(
$query->expr()->eq('e.active', "'TRUE'")
);
}
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator, $_format)
{
return $query->addOrderBy('e.order', 'ASC');
}
}

View File

@@ -0,0 +1,26 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class GenderController extends CRUDController
{
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
$query->addOrderBy('e.order', 'ASC');
return parent::orderQuery($action, $query, $request, $paginator);
}
}

View File

@@ -24,7 +24,7 @@ class NewsItemApiController
public function __construct(
private readonly NewsItemRepository $newsItemRepository,
private readonly SerializerInterface $serializer,
private readonly PaginatorFactory $paginatorFactory
private readonly PaginatorFactory $paginatorFactory,
) {}
/**

View File

@@ -94,4 +94,38 @@ class NotificationApiController
return new JsonResponse(null, JsonResponse::HTTP_ACCEPTED, [], false);
}
/**
* @Route("/mark/allread", name="chill_api_main_notification_mark_allread", methods={"POST"})
*/
public function markAllRead(): JsonResponse
{
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new \RuntimeException('Invalid user');
}
$modifiedNotificationIds = $this->notificationRepository->markAllNotificationAsReadForUser($user);
return new JsonResponse($modifiedNotificationIds);
}
/**
* @Route("/mark/undoallread", name="chill_api_main_notification_mark_undoallread", methods={"POST"})
*/
public function undoAllRead(Request $request): JsonResponse
{
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new \RuntimeException('Invalid user');
}
$ids = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
$touchedIds = $this->notificationRepository->markAllNotificationAsUnreadForUser($user, $ids);
return new JsonResponse($touchedIds);
}
}

View File

@@ -169,7 +169,7 @@ class NotificationController extends AbstractController
#[Route(path: '/inbox', name: 'chill_main_notification_my')]
public function inboxAction(): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$this->denyAccessUnlessGranted('ROLE_USER');
$currentUser = $this->security->getUser();
$notificationsNbr = $this->notificationRepository->countAllForAttendee($currentUser);
@@ -177,8 +177,8 @@ class NotificationController extends AbstractController
$notifications = $this->notificationRepository->findAllForAttendee(
$currentUser,
$limit = $paginator->getItemsPerPage(),
$offset = $paginator->getCurrentPage()->getFirstItemNumber()
$paginator->getItemsPerPage(),
$paginator->getCurrentPage()->getFirstItemNumber()
);
return $this->render('@ChillMain/Notification/list.html.twig', [
@@ -309,6 +309,7 @@ class NotificationController extends AbstractController
$templateData[] = [
'template' => $this->notificationHandlerManager->getTemplate($notification),
'template_data' => $this->notificationHandlerManager->getTemplateData($notification),
'handler' => $this->notificationHandlerManager->getHandler($notification),
'notification' => $notification,
];
}

View File

@@ -278,7 +278,7 @@ final class PasswordController extends AbstractController
}
/**
* @return \Symfony\Component\Form\Form
* @return \Symfony\Component\Form\FormInterface
*/
private function passwordForm(User $user)
{

View File

@@ -61,8 +61,6 @@ final class PermissionsGroupController extends AbstractController
$form = $this->createAddRoleScopeForm($permissionsGroup);
$form->handleRequest($request);
dump($form->isSubmitted());
if ($form->isSubmitted() && $form->isValid()) {
$roleScope = $this->getPersistentRoleScopeBy(
$form['composed_role_scope']->getData()->getRole(),
@@ -153,7 +151,7 @@ final class PermissionsGroupController extends AbstractController
/**
* remove an association between permissionsGroup and roleScope.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/permissionsgroup/{pgid}/delete_link_role_scope/{rsid}', name: 'admin_permissionsgroup_delete_role_scope', methods: ['DELETE'])]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/permissionsgroup/{pgid}/delete_link_role_scope/{rsid}', name: 'admin_permissionsgroup_delete_role_scope', methods: ['POST'])]
public function deleteLinkRoleScopeAction(int $pgid, int $rsid): Response
{
$permissionsGroup = $this->permissionsGroupRepository->find($pgid);
@@ -445,7 +443,7 @@ final class PermissionsGroupController extends AbstractController
*/
private function createDeleteRoleScopeForm(
PermissionsGroup $permissionsGroup,
RoleScope $roleScope
RoleScope $roleScope,
): FormInterface {
return $this->createFormBuilder()
->setAction($this->generateUrl(

View File

@@ -27,7 +27,7 @@ class ScopeController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
) {}
/**

View File

@@ -96,7 +96,7 @@ class SearchController extends AbstractController
return $this->render('@ChillMain/Search/choose_list.html.twig');
}
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json'], defaults: ['_format' => 'html'])]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json', '_locale' => '[a-z]{1,3}'], defaults: ['_format' => 'html'])]
public function searchAction(Request $request, mixed $_format)
{
$pattern = trim((string) $request->query->get('q', ''));

View File

@@ -20,7 +20,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class UIController extends AbstractController
{
public function showNotificationUserCounterAction(
CountNotificationUser $counter
CountNotificationUser $counter,
) {
$nb = $counter->getSumNotification($this->getUser());

View File

@@ -12,7 +12,9 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\PermissionsGroup;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ComposedGroupCenterType;
use Chill\MainBundle\Form\UserCurrentLocationType;
@@ -46,7 +48,7 @@ class UserController extends CRUDController
protected ParameterBagInterface $parameterBag,
private readonly TranslatorInterface $translator,
private readonly ChillSecurity $security,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
) {}
#[Route(path: '/{_locale}/admin/main/user/{uid}/add_link_groupcenter', name: 'admin_user_add_groupcenter')]
@@ -64,10 +66,14 @@ class UserController extends CRUDController
$form->handleRequest($request);
if ($form->isValid()) {
$groupCenter = $this->getPersistedGroupCenter(
$form[self::FORM_GROUP_CENTER_COMPOSED]->getData()
);
$user->addGroupCenter($groupCenter);
$formData = $form[self::FORM_GROUP_CENTER_COMPOSED]->getData();
$selectedCenters = $formData['center'];
foreach ($selectedCenters as $center) {
$groupCenter = $this->getPersistedGroupCenter($center, $formData['permissionsgroup']);
$user->addGroupCenter($groupCenter);
}
if (0 === $this->validator->validate($user)->count()) {
$em->flush();
@@ -214,7 +220,7 @@ class UserController extends CRUDController
return $this->redirect(
$request->query->has('returnPath') ? $request->query->get('returnPath') :
$this->generateUrl('chill_main_homepage')
$this->generateUrl('chill_main_homepage')
);
}
@@ -249,7 +255,7 @@ class UserController extends CRUDController
return $this->redirect(
$request->query->has('returnPath') ? $request->query->get('returnPath') :
$this->generateUrl('chill_crud_admin_user_edit', ['id' => $user->getId()])
$this->generateUrl('chill_crud_admin_user_edit', ['id' => $user->getId()])
);
}
@@ -264,6 +270,7 @@ class UserController extends CRUDController
return $this->getFilterOrderHelperFactory()
->create(self::class)
->addSearchBox(['label'])
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
->build();
}
@@ -273,11 +280,7 @@ class UserController extends CRUDController
return parent::countEntities($action, $request, $filterOrder);
}
if (null === $filterOrder->getQueryString()) {
return parent::countEntities($action, $request, $filterOrder);
}
return $this->userRepository->countByUsernameOrEmail($filterOrder->getQueryString());
return $this->userRepository->countFilteredUsers($filterOrder->getQueryString(), $filterOrder->getCheckboxData('activeFilter'));
}
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
@@ -324,7 +327,7 @@ class UserController extends CRUDController
Request $request,
int $totalItems,
PaginatorInterface $paginator,
?FilterOrderHelper $filterOrder = null
?FilterOrderHelper $filterOrder = null,
) {
if (0 === $totalItems) {
return [];
@@ -334,16 +337,13 @@ class UserController extends CRUDController
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
}
if (null === $filterOrder->getQueryString()) {
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
}
$queryString = $filterOrder->getQueryString();
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
$nb = $this->userRepository->countFilteredUsers($queryString, $activeFilter);
return $this->userRepository->findByUsernameOrEmail(
$filterOrder->getQueryString(),
['usernameCanonical' => 'ASC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
$paginator = $this->getPaginatorFactory()->create($nb);
return $this->userRepository->findFilteredUsers($queryString, $activeFilter, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage());
}
protected function onPrePersist(string $action, $entity, FormInterface $form, Request $request)
@@ -374,10 +374,12 @@ class UserController extends CRUDController
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
return $this->createFormBuilder()
->setAction($this->generateUrl(
'admin_user_add_groupcenter',
array_merge($returnPathParams, ['uid' => $user->getId()])
))
->setAction(
$this->generateUrl(
'admin_user_add_groupcenter',
array_merge($returnPathParams, ['uid' => $user->getId()])
)
)
->setMethod('POST')
->add(self::FORM_GROUP_CENTER_COMPOSED, ComposedGroupCenterType::class)
->add('submit', SubmitType::class, ['label' => 'Add a new groupCenter'])
@@ -392,10 +394,12 @@ class UserController extends CRUDController
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
return $this->createFormBuilder()
->setAction($this->generateUrl(
'admin_user_delete_groupcenter',
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])
))
->setAction(
$this->generateUrl(
'admin_user_delete_groupcenter',
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])
)
)
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
@@ -421,17 +425,21 @@ class UserController extends CRUDController
}
}
private function getPersistedGroupCenter(GroupCenter $groupCenter)
private function getPersistedGroupCenter(Center $center, PermissionsGroup $permissionsGroup)
{
$em = $this->managerRegistry->getManager();
$groupCenterManaged = $em->getRepository(GroupCenter::class)
->findOneBy([
'center' => $groupCenter->getCenter(),
'permissionsGroup' => $groupCenter->getPermissionsGroup(),
'center' => $center,
'permissionsGroup' => $permissionsGroup,
]);
if (!$groupCenterManaged) {
$groupCenter = new GroupCenter();
$groupCenter->setCenter($center);
$groupCenter->setPermissionsGroup($permissionsGroup);
$em->persist($groupCenter);
return $groupCenter;

View File

@@ -0,0 +1,28 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class UserGroupAdminController extends CRUDController
{
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
$query->addSelect('JSON_EXTRACT(e.label, :lang) AS HIDDEN labeli18n')
->setParameter('lang', $request->getLocale());
$query->addOrderBy('labeli18n', 'ASC');
return $query;
}
}

View File

@@ -0,0 +1,177 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Repository\UserGroupRepositoryInterface;
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
use Chill\MainBundle\Security\Authorization\UserGroupVoter;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Translation\TranslatableMessage;
use Twig\Environment;
/**
* Controller to see and manage user groups.
*/
final readonly class UserGroupController
{
public function __construct(
private UserGroupRepositoryInterface $userGroupRepository,
private Security $security,
private PaginatorFactoryInterface $paginatorFactory,
private Environment $twig,
private FormFactoryInterface $formFactory,
private ChillUrlGeneratorInterface $chillUrlGenerator,
private EntityManagerInterface $objectManager,
private ChillEntityRenderManagerInterface $chillEntityRenderManager,
) {}
#[Route('/{_locale}/main/user-groups/my', name: 'chill_main_user_groups_my')]
public function myUserGroups(): Response
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException();
}
$nb = $this->userGroupRepository->countByUser($user);
$paginator = $this->paginatorFactory->create($nb);
$groups = $this->userGroupRepository->findByUser($user, true, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
$forms = new \SplObjectStorage();
foreach ($groups as $group) {
$forms->attach($group, $this->createFormAppendUserForGroup($group)?->createView());
}
return new Response($this->twig->render('@ChillMain/UserGroup/my_user_groups.html.twig', [
'groups' => $groups,
'paginator' => $paginator,
'forms' => $forms,
]));
}
#[Route('/{_locale}/main/user-groups/{id}/append', name: 'chill_main_user_groups_append_users')]
public function appendUsersToGroup(UserGroup $userGroup, Request $request, Session $session): Response
{
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) {
throw new AccessDeniedHttpException();
}
$form = $this->createFormAppendUserForGroup($userGroup);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
foreach ($form['users']->getData() as $user) {
$userGroup->addUser($user);
$session->getFlashBag()->add(
'success',
new TranslatableMessage(
'user_group.user_added',
[
'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []),
'user' => $this->chillEntityRenderManager->renderString($user, []),
]
)
);
}
$this->objectManager->flush();
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
);
}
if ($form->isSubmitted()) {
$errors = [];
foreach ($form->getErrors() as $error) {
$errors[] = $error->getMessage();
}
return new Response(implode(', ', $errors));
}
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
);
}
/**
* @ParamConverter("user", class=User::class, options={"id" = "userId"})
*/
#[Route('/{_locale}/main/user-group/{id}/user/{userId}/remove', name: 'chill_main_user_groups_remove_user')]
public function removeUserToGroup(UserGroup $userGroup, User $user, Session $session): Response
{
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) {
throw new AccessDeniedHttpException();
}
$userGroup->removeUser($user);
$this->objectManager->flush();
$session->getFlashBag()->add(
'success',
new TranslatableMessage(
'user_group.user_removed',
[
'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []),
'user' => $this->chillEntityRenderManager->renderString($user, []),
]
)
);
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
);
}
private function createFormAppendUserForGroup(UserGroup $group): ?FormInterface
{
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $group)) {
return null;
}
$builder = $this->formFactory->createBuilder(FormType::class, ['users' => []], [
'action' => $this->chillUrlGenerator->generateWithReturnPath('chill_main_user_groups_append_users', ['id' => $group->getId()]),
]);
$builder->add('users', PickUserDynamicType::class, [
'submit_on_adding_new_entity' => true,
'label' => 'user_group.append_users',
'mapped' => false,
'multiple' => true,
]);
return $builder->getForm();
}
}

View File

@@ -0,0 +1,79 @@
<?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\MainBundle\Controller;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Twig\Environment;
final readonly class WorkflowAddSignatureController
{
public function __construct(
private EntityWorkflowManager $entityWorkflowManager,
private PDFSignatureZoneAvailable $PDFSignatureZoneAvailable,
private NormalizerInterface $normalizer,
private Environment $twig,
private UrlGeneratorInterface $urlGenerator,
private Security $security,
) {}
#[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature_add', methods: 'GET')]
public function __invoke(EntityWorkflowStepSignature $signature, Request $request): Response
{
if (!$this->security->isGranted(EntityWorkflowStepSignatureVoter::SIGN, $signature)) {
throw new AccessDeniedHttpException('not authorized to sign this step');
}
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
return new RedirectResponse(
$this->urlGenerator->generate('chill_main_workflow_show', ['id' => $entityWorkflow->getId()])
);
}
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
if (null === $storedObject) {
throw new NotFoundHttpException('No stored object found');
}
$zones = $this->PDFSignatureZoneAvailable->getAvailableSignatureZones($entityWorkflow);
$signatureClient = [];
$signatureClient['id'] = $signature->getId();
$signatureClient['storedObject'] = $this->normalizer->normalize($storedObject, 'json');
$signatureClient['zones'] = $zones;
return new Response(
$this->twig->render(
'@ChillMain/Workflow/signature_sign.html.twig',
['signature' => $signatureClient]
)
);
}
}

View File

@@ -0,0 +1,101 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
use Chill\MainBundle\Workflow\Attachment\AddAttachmentAction;
use Chill\MainBundle\Workflow\Attachment\AddAttachmentRequestDTO;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
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\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class WorkflowAttachmentController
{
public function __construct(
private readonly Security $security,
private readonly SerializerInterface $serializer,
private readonly ValidatorInterface $validator,
private readonly EntityManagerInterface $entityManager,
private readonly AddAttachmentAction $addAttachmentAction,
) {}
#[Route('/api/1.0/main/workflow/{id}/attachment', methods: ['POST'])]
public function addAttachment(EntityWorkflow $entityWorkflow, Request $request): JsonResponse
{
if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $entityWorkflow)) {
throw new AccessDeniedHttpException();
}
$dto = new AddAttachmentRequestDTO($entityWorkflow);
$this->serializer->deserialize($request->getContent(), AddAttachmentRequestDTO::class, 'json', [
AbstractNormalizer::OBJECT_TO_POPULATE => $dto, AbstractNormalizer::GROUPS => ['write'],
]);
$errors = $this->validator->validate($dto);
if (count($errors) > 0) {
return new JsonResponse(
$this->serializer->serialize($errors, 'json'),
Response::HTTP_UNPROCESSABLE_ENTITY,
json: true
);
}
$attachment = ($this->addAttachmentAction)($dto);
$this->entityManager->flush();
return new JsonResponse(
$this->serializer->serialize($attachment, 'json', [AbstractNormalizer::GROUPS => ['read']]),
json: true
);
}
#[Route('/api/1.0/main/workflow/attachment/{id}', methods: ['DELETE'])]
public function removeAttachment(EntityWorkflowAttachment $attachment): Response
{
if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $attachment->getEntityWorkflow())) {
throw new AccessDeniedHttpException();
}
$this->entityManager->remove($attachment);
$this->entityManager->flush();
return new Response(null, Response::HTTP_NO_CONTENT);
}
#[Route('/api/1.0/main/workflow/{id}/attachment', methods: ['GET'])]
public function listAttachmentsForEntityWorkflow(EntityWorkflow $entityWorkflow): JsonResponse
{
if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $entityWorkflow)) {
throw new AccessDeniedHttpException();
}
return new JsonResponse(
$this->serializer->serialize(
$entityWorkflow->getAttachments(),
'json',
[AbstractNormalizer::GROUPS => ['read']]
),
json: true
);
}
}

View File

@@ -13,23 +13,27 @@ namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Form\EntityWorkflowCommentType;
use Chill\MainBundle\Form\WorkflowSignatureMetadataType;
use Chill\MainBundle\Form\WorkflowStepType;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
use Chill\MainBundle\Security\ChillSecurity;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Registry;
@@ -38,7 +42,19 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class WorkflowController extends AbstractController
{
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly ValidatorInterface $validator, private readonly PaginatorFactory $paginatorFactory, private readonly Registry $registry, private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, private readonly ChillSecurity $security, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry) {}
public function __construct(
private readonly EntityWorkflowManager $entityWorkflowManager,
private readonly EntityWorkflowRepository $entityWorkflowRepository,
private readonly ValidatorInterface $validator,
private readonly PaginatorFactory $paginatorFactory,
private readonly Registry $registry,
private readonly EntityManagerInterface $entityManager,
private readonly TranslatorInterface $translator,
private readonly ChillSecurity $security,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
private readonly ClockInterface $clock,
private readonly EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
) {}
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
public function create(Request $request): Response
@@ -268,6 +284,9 @@ class WorkflowController extends AbstractController
);
}
/**
* @throws NonUniqueResultException
*/
#[Route(path: '/{_locale}/main/workflow/{id}/show', name: 'chill_main_workflow_show')]
public function show(EntityWorkflow $entityWorkflow, Request $request): Response
{
@@ -276,24 +295,17 @@ class WorkflowController extends AbstractController
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$errors = [];
$signatures = $entityWorkflow->getCurrentStep()->getSignatures();
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
// possible transition
$usersInvolved = $entityWorkflow->getUsersInvolved();
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
if (false !== $currentUserFound) {
unset($usersInvolved[$currentUserFound]);
}
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
$transitionForm = $this->createForm(
WorkflowStepType::class,
$entityWorkflow->getCurrentStep(),
$stepDTO,
[
'transition' => true,
'entity_workflow' => $entityWorkflow,
'suggested_users' => $usersInvolved,
]
);
@@ -310,12 +322,14 @@ class WorkflowController extends AbstractController
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
}
// TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
$byUser = $this->security->getUser();
$workflow->apply($entityWorkflow, $transition);
$workflow->apply($entityWorkflow, $transition, [
'context' => $stepDTO,
'byUser' => $byUser,
'transition' => $transition,
'transitionAt' => $this->clock->now(),
]);
$this->entityManager->flush();
@@ -327,22 +341,6 @@ class WorkflowController extends AbstractController
}
}
/*
$commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment());
$commentForm->handleRequest($request);
if ($commentForm->isSubmitted() && $commentForm->isValid()) {
$this->entityManager->persist($newComment);
$this->entityManager->flush();
$this->addFlash('success', $this->translator->trans('workflow.Comment added'));
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
} elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
*/
return $this->render(
'@ChillMain/Workflow/index.html.twig',
[
@@ -352,7 +350,8 @@ class WorkflowController extends AbstractController
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
'entity_workflow' => $entityWorkflow,
'transition_form_errors' => $errors,
// 'comment_form' => $commentForm->createView(),
'signatures' => $signatures,
'related_accompanying_period' => $this->entityWorkflowManager->getRelatedAccompanyingPeriod($entityWorkflow),
]
);
}
@@ -371,4 +370,65 @@ class WorkflowController extends AbstractController
return $lines;
}
#[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/metadata', name: 'chill_main_workflow_signature_metadata')]
public function addSignatureMetadata(int $signature_id, Request $request): Response
{
$signature = $this->entityWorkflowStepSignatureRepository->find($signature_id);
if (null === $signature) {
throw new NotFoundHttpException('signature not found');
}
if ($signature->isSigned()) {
$this->addFlash(
'notice',
$this->translator->trans('workflow.signature_zone.already_signed_alert')
);
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]);
}
if ($signature->getSigner() instanceof User) {
return $this->redirectToRoute('chill_main_workflow_signature_add', [
'id' => $signature_id,
'returnPath' => $request->query->get('returnPath', null),
]);
}
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
$metadataForm->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
$metadataForm->handleRequest($request);
if ($metadataForm->isSubmitted() && $metadataForm->isValid()) {
$data = $metadataForm->getData();
$signature->setSignatureMetadata(
[
'base_signer' => [
'document_type' => $data['documentType'],
'document_number' => $data['documentNumber'],
'expiration_date' => $data['expirationDate'],
],
]
);
$this->entityManager->persist($signature);
$this->entityManager->flush();
return $this->redirectToRoute('chill_main_workflow_signature_add', [
'id' => $signature_id,
'returnPath' => $request->query->get('returnPath', null),
]);
}
return $this->render(
'@ChillMain/Workflow/signature_metadata.html.twig',
[
'metadata_form' => $metadataForm->createView(),
'person' => $signature->getSigner(),
]
);
}
}

View File

@@ -0,0 +1,98 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepHold;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Registry;
class WorkflowOnHoldController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly Security $security,
private readonly Registry $registry,
private readonly UrlGeneratorInterface $urlGenerator,
) {}
#[Route(path: '/{_locale}/main/workflow/{id}/hold', name: 'chill_main_workflow_on_hold')]
public function putOnHold(EntityWorkflow $entityWorkflow, Request $request): Response
{
$currentStep = $entityWorkflow->getCurrentStep();
$currentUser = $this->security->getUser();
if (!$currentUser instanceof User) {
throw new AccessDeniedHttpException('only user can put a workflow on hold');
}
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$enabledTransitions = $workflow->getEnabledTransitions($entityWorkflow);
if (0 === count($enabledTransitions)) {
throw new AccessDeniedHttpException('You are not allowed to apply any transitions to this workflow, therefore you cannot toggle the hold status.');
}
$stepHold = new EntityWorkflowStepHold($currentStep, $currentUser);
$this->entityManager->persist($stepHold);
$this->entityManager->flush();
return new RedirectResponse(
$this->urlGenerator->generate(
'chill_main_workflow_show',
['id' => $entityWorkflow->getId()]
)
);
}
#[Route(path: '/{_locale}/main/workflow/{id}/remove_hold', name: 'chill_main_workflow_remove_hold')]
public function removeOnHold(EntityWorkflowStep $entityWorkflowStep): Response
{
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException('only user can remove workflow on hold');
}
if (!$entityWorkflowStep->isOnHoldByUser($user)) {
throw new AccessDeniedHttpException('You are not allowed to remove workflow on hold');
}
$hold = $entityWorkflowStep->getHoldsOnStep()->findFirst(fn (int $index, EntityWorkflowStepHold $entityWorkflowStepHold) => $user === $entityWorkflowStepHold->getByUser());
if (null === $hold) {
// this should not happens...
throw new NotFoundHttpException();
}
$this->entityManager->remove($hold);
$this->entityManager->flush();
return new RedirectResponse(
$this->urlGenerator->generate(
'chill_main_workflow_show',
['id' => $entityWorkflowStep->getEntityWorkflow()->getId()]
)
);
}
}

View File

@@ -0,0 +1,99 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
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\Security\Core\Security;
use Twig\Environment;
final readonly class WorkflowSignatureStateChangeController
{
public function __construct(
private EntityManagerInterface $entityManager,
private Security $security,
private FormFactoryInterface $formFactory,
private Environment $twig,
private SignatureStepStateChanger $signatureStepStateChanger,
private ChillUrlGeneratorInterface $chillUrlGenerator,
) {}
#[Route('/{_locale}/main/workflow/signature/{id}/cancel', name: 'chill_main_workflow_signature_cancel')]
public function cancelSignature(EntityWorkflowStepSignature $signature, Request $request): Response
{
return $this->markSignatureAction(
$signature,
$request,
EntityWorkflowStepSignatureVoter::CANCEL,
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); },
'@ChillMain/WorkflowSignature/cancel.html.twig',
);
}
#[Route('/{_locale}/main/workflow/signature/{id}/reject', name: 'chill_main_workflow_signature_reject')]
public function rejectSignature(EntityWorkflowStepSignature $signature, Request $request): Response
{
return $this->markSignatureAction(
$signature,
$request,
EntityWorkflowStepSignatureVoter::REJECT,
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); },
'@ChillMain/WorkflowSignature/reject.html.twig',
);
}
private function markSignatureAction(
EntityWorkflowStepSignature $signature,
Request $request,
string $permissionAttribute,
callable $markSignature,
string $template,
): Response {
if (!$this->security->isGranted($permissionAttribute, $signature)) {
throw new AccessDeniedHttpException('not allowed to cancel this signature');
}
$form = $this->formFactory->create();
$form->add('confirm', SubmitType::class, ['label' => 'Confirm']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->wrapInTransaction(function () use ($signature, $markSignature) {
$markSignature($signature);
});
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
);
}
return
new Response(
$this->twig->render(
$template,
['form' => $form->createView(), 'signature' => $signature]
)
);
}
}

View File

@@ -0,0 +1,89 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException;
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
final readonly class WorkflowViewSendPublicController
{
public const LOG_PREFIX = '[workflow-view-send-public-controller] ';
public function __construct(
private EntityManagerInterface $entityManager,
private LoggerInterface $chillLogger,
private EntityWorkflowManager $entityWorkflowManager,
private ClockInterface $clock,
private Environment $environment,
private MessageBusInterface $messageBus,
) {}
#[Route('/public/main/workflow/send/{uuid}/view/{verificationKey}', name: 'chill_main_workflow_send_view_public', methods: ['GET'])]
public function __invoke(EntityWorkflowSend $workflowSend, string $verificationKey, Request $request): Response
{
if (50 < $workflowSend->getNumberOfErrorTrials()) {
throw new AccessDeniedHttpException('number of trials exceeded, no more access allowed');
}
if ($verificationKey !== $workflowSend->getPrivateToken()) {
$this->chillLogger->info(self::LOG_PREFIX.'Invalid trial for this send', ['client_ip' => $request->getClientIp()]);
$workflowSend->increaseErrorTrials();
$this->entityManager->flush();
throw new AccessDeniedHttpException('invalid verification key');
}
if ($this->clock->now() > $workflowSend->getExpireAt()) {
return new Response(
$this->environment->render('@ChillMain/Workflow/workflow_view_send_public_expired.html.twig'),
409
);
}
if (100 < $workflowSend->getViews()->count()) {
$this->chillLogger->info(self::LOG_PREFIX.'100 view reached, not allowed to see it again');
throw new AccessDeniedHttpException('100 views reached, not allowed to see it again');
}
try {
$metadata = new EntityWorkflowViewMetadataDTO(
$workflowSend->getViews()->count(),
100 - $workflowSend->getViews()->count(),
);
$response = new Response(
$this->entityWorkflowManager->renderPublicView($workflowSend, $metadata),
);
$view = new EntityWorkflowSendView($workflowSend, $this->clock->now(), $request->getClientIp());
$this->entityManager->persist($view);
$this->messageBus->dispatch(new PostPublicViewMessage($view->getId()));
$this->entityManager->flush();
return $response;
} catch (HandlerWithPublicViewNotFoundException $e) {
throw new \RuntimeException('Could not render the public view', previous: $e);
}
}
}

View File

@@ -24,9 +24,9 @@ interface CronJobInterface
*
* If data is returned, this data is passed as argument on the next execution
*
* @param array $lastExecutionData the data which was returned from the previous execution
* @param array<string|int, int|float|string|bool|array<int|string, int|float|string|bool>> $lastExecutionData the data which was returned from the previous execution
*
* @return array|null optionally return an array with the same data than the previous execution
* @return array<string|int, int|float|string|bool|array<int|string, int|float|string|bool>>|null optionally return an array with the same data than the previous execution
*/
public function run(array $lastExecutionData): ?array;
}

View File

@@ -54,7 +54,7 @@ final readonly class CronManager implements CronManagerInterface
private CronJobExecutionRepositoryInterface $cronJobExecutionRepository,
private EntityManagerInterface $entityManager,
private iterable $jobs,
private LoggerInterface $logger
private LoggerInterface $logger,
) {}
public function run(?string $forceJob = null): void

View File

@@ -19,25 +19,25 @@ use Doctrine\Persistence\ObjectManager;
*/
trait LoadAbstractNotificationsTrait
{
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
return;
foreach ($this->notifs as $notif) {
$entityId = $this->getReference($notif['entityRef'])->getId();
$entityId = $this->getReference($notif['entityRef'], Notification::class)->getId();
echo 'Adding notification for '.$notif['entityClass'].'(entity id:'.$entityId.")\n";
$newNotif = (new Notification())
->setMessage($notif['message'])
->setSender($this->getReference($notif['sender']))
->setSender($this->getReference($notif['sender'], Notification::class))
->setRelatedEntityClass($notif['entityClass'])
->setRelatedEntityId($entityId)
->setDate(new \DateTimeImmutable('now'))
->setRead([]);
foreach ($notif['addressees'] as $addressee) {
$newNotif->addAddressee($this->getReference($addressee));
$newNotif->addAddressee($this->getReference($addressee, Notification::class));
}
$manager->persist($newNotif);

View File

@@ -13,6 +13,7 @@ namespace Chill\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Doctrine\Model\Point;
use Chill\MainBundle\Entity\AddressReference;
use Chill\MainBundle\Entity\PostalCode;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
@@ -33,12 +34,12 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt
$this->faker = \Faker\Factory::create('fr_FR');
}
public function getOrder()
public function getOrder(): int
{
return 51;
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
echo "loading some reference address... \n";
@@ -69,7 +70,8 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt
$ar->setStreetNumber((string) random_int(0, 199));
$ar->setPoint($this->getRandomPoint());
$ar->setPostcode($this->getReference(
LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)]
LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)],
PostalCode::class
));
$ar->setMunicipalityCode($ar->getPostcode()->getCode());

View File

@@ -31,12 +31,12 @@ class LoadCenters extends AbstractFixture implements OrderedFixtureInterface
public static $refs = [];
public function getOrder()
public function getOrder(): int
{
return 100;
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
foreach (static::$centers as $new) {
$center = new Center();

View File

@@ -25,12 +25,12 @@ class LoadCountries extends AbstractFixture implements ContainerAwareInterface,
{
private ?ContainerInterface $container = null;
public function getOrder()
public function getOrder(): int
{
return 20;
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
echo "loading countries... \n";

View File

@@ -0,0 +1,63 @@
<?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\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Entity\GenderIconEnum;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
class LoadGenders extends AbstractFixture implements OrderedFixtureInterface
{
private array $genders = [
[
'label' => ['en' => 'man', 'fr' => 'homme'],
'genderTranslation' => GenderEnum::MALE,
'icon' => GenderIconEnum::MALE,
],
[
'label' => ['en' => 'woman', 'fr' => 'femme'],
'genderTranslation' => GenderEnum::FEMALE,
'icon' => GenderIconEnum::FEMALE,
],
[
'label' => ['en' => 'neutral', 'fr' => 'neutre'],
'genderTranslation' => GenderEnum::NEUTRAL,
'icon' => GenderIconEnum::NEUTRAL,
],
];
public function getOrder(): int
{
return 100;
}
public function load(ObjectManager $manager): void
{
echo "loading genders... \n";
foreach ($this->genders as $g) {
echo $g['label']['fr'].' ';
$new_g = new Gender();
$new_g->setGenderTranslation($g['genderTranslation']);
$new_g->setLabel($g['label']);
$new_g->setIcon($g['icon']);
$this->addReference('g_'.$g['genderTranslation']->value, $new_g);
$manager->persist($new_g);
}
$manager->flush();
}
}

View File

@@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\PermissionsGroup;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
@@ -20,18 +22,18 @@ class LoadGroupCenters extends AbstractFixture implements OrderedFixtureInterfac
{
public static $refs = [];
public function getOrder()
public function getOrder(): int
{
return 500;
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
foreach (LoadCenters::$refs as $centerRef) {
foreach (LoadPermissionsGroup::$refs as $permissionGroupRef) {
$GroupCenter = new GroupCenter();
$GroupCenter->setCenter($this->getReference($centerRef));
$GroupCenter->setPermissionsGroup($this->getReference($permissionGroupRef));
$GroupCenter->setCenter($this->getReference($centerRef, Center::class));
$GroupCenter->setPermissionsGroup($this->getReference($permissionGroupRef, PermissionsGroup::class));
$manager->persist($GroupCenter);

View File

@@ -33,12 +33,12 @@ class LoadLanguages extends AbstractFixture implements ContainerAwareInterface,
// This array contains regional code to not exclude
private array $regionalVersionToInclude = ['ro_MD'];
public function getOrder()
public function getOrder(): int
{
return 10;
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
echo "loading languages... \n";

View File

@@ -25,7 +25,7 @@ class LoadLocationType extends AbstractFixture implements ContainerAwareInterfac
{
private ?ContainerInterface $container = null;
public function getOrder()
public function getOrder(): int
{
return 52;
}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\PermissionsGroup;
use Chill\MainBundle\Entity\RoleScope;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
@@ -47,19 +48,19 @@ class LoadPermissionsGroup extends AbstractFixture implements OrderedFixtureInte
public static $refs = [];
public function getOrder()
public function getOrder(): int
{
return 400;
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
foreach (static::$permissionGroup as $new) {
$permissionGroup = new PermissionsGroup();
$permissionGroup->setName($new['name']);
foreach ($new['role_scopes'] as $roleScopeRef) {
$permissionGroup->addRoleScope($this->getReference($roleScopeRef));
$permissionGroup->addRoleScope($this->getReference($roleScopeRef, RoleScope::class));
}
$manager->persist($permissionGroup);

View File

@@ -325,12 +325,12 @@ class LoadPostalCodes extends AbstractFixture implements OrderedFixtureInterface
85800,GIVRAND,FR,85100,46.6822701061,-1.8787272243,INSEE
EOF;
public function getOrder()
public function getOrder(): int
{
return 50;
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
echo "loading postal codes... \n";
$this->loadPostalCodeCSV($manager, self::$postalCodeBelgium, 'BE');
@@ -365,7 +365,7 @@ class LoadPostalCodes extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($c);
$ref = 'postal_code_'.$code[0];
if (!$this->hasReference($ref)) {
if (!$this->hasReference($ref, PostalCode::class)) {
$this->addReference($ref, $c);
self::$refs[] = $ref;
}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\RoleScope;
use Chill\MainBundle\Entity\Scope;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
@@ -44,19 +45,19 @@ class LoadRoleScopes extends AbstractFixture implements OrderedFixtureInterface
public static $references = [];
public function getOrder()
public function getOrder(): int
{
return 300;
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
foreach (static::$permissions as $key => $permission) {
foreach (LoadScopes::$references as $scopeReference) {
$roleScope = new RoleScope();
$roleScope->setRole($key)
->setScope($this->getReference($scopeReference));
$reference = 'role_scope_'.$key.'_'.$this->getReference($scopeReference)->getName()['en'];
->setScope($this->getReference($scopeReference, Scope::class));
$reference = 'role_scope_'.$key.'_'.$this->getReference($scopeReference, Scope::class)->getName()['en'];
echo "Creating {$reference} \n";
$this->addReference($reference, $roleScope);
$manager->persist($roleScope);

View File

@@ -46,12 +46,12 @@ class LoadScopes extends AbstractFixture implements OrderedFixtureInterface
],
];
public function getOrder()
public function getOrder(): int
{
return 200;
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
foreach ($this->scopes as $new) {
$scope = new \Chill\MainBundle\Entity\Scope();

View File

@@ -24,7 +24,7 @@ class LoadUserGroup extends Fixture implements FixtureGroupInterface
return ['user-group'];
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
$centerASocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_social']);
$centerBSocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_social']);

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\User;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
@@ -54,12 +55,12 @@ class LoadUsers extends AbstractFixture implements ContainerAwareInterface, Orde
private ?ContainerInterface $container = null;
public function getOrder()
public function getOrder(): int
{
return 1000;
}
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
foreach (self::$refs as $username => $params) {
$user = new User();
@@ -80,7 +81,7 @@ class LoadUsers extends AbstractFixture implements ContainerAwareInterface, Orde
->setEmail(sprintf('%s@chill.social', \str_replace(' ', '', (string) $username)));
foreach ($params['groupCenterRefs'] as $groupCenterRef) {
$user->addGroupCenter($this->getReference($groupCenterRef));
$user->addGroupCenter($this->getReference($groupCenterRef, GroupCenter::class));
}
echo 'Creating user '.$username."... \n";

View File

@@ -17,6 +17,8 @@ use Chill\MainBundle\Controller\CivilityApiController;
use Chill\MainBundle\Controller\CivilityController;
use Chill\MainBundle\Controller\CountryApiController;
use Chill\MainBundle\Controller\CountryController;
use Chill\MainBundle\Controller\GenderApiController;
use Chill\MainBundle\Controller\GenderController;
use Chill\MainBundle\Controller\GeographicalUnitApiController;
use Chill\MainBundle\Controller\LanguageController;
use Chill\MainBundle\Controller\LocationController;
@@ -24,6 +26,7 @@ use Chill\MainBundle\Controller\LocationTypeController;
use Chill\MainBundle\Controller\NewsItemController;
use Chill\MainBundle\Controller\RegroupmentController;
use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Controller\UserGroupAdminController;
use Chill\MainBundle\Controller\UserGroupApiController;
use Chill\MainBundle\Controller\UserJobApiController;
use Chill\MainBundle\Controller\UserJobController;
@@ -53,6 +56,7 @@ use Chill\MainBundle\Doctrine\Type\PointType;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GeographicalUnitLayer;
use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Entity\Location;
@@ -65,11 +69,13 @@ use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Form\CenterType;
use Chill\MainBundle\Form\CivilityType;
use Chill\MainBundle\Form\CountryType;
use Chill\MainBundle\Form\GenderType;
use Chill\MainBundle\Form\LanguageType;
use Chill\MainBundle\Form\LocationFormType;
use Chill\MainBundle\Form\LocationTypeType;
use Chill\MainBundle\Form\NewsItemType;
use Chill\MainBundle\Form\RegroupmentType;
use Chill\MainBundle\Form\UserGroupType;
use Chill\MainBundle\Form\UserJobType;
use Chill\MainBundle\Form\UserType;
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
@@ -228,6 +234,8 @@ class ChillMainExtension extends Extension implements
public function prepend(ContainerBuilder $container)
{
$this->prependNotifierTexterWithLegacyData($container);
// add installation_name and date_format to globals
$chillMainConfig = $container->getExtensionConfig($this->getAlias());
$config = $this->processConfiguration($this
@@ -337,7 +345,7 @@ class ChillMainExtension extends Extension implements
ContainerBuilder $container,
array $crudConfig,
array $apiConfig,
Loader\YamlFileLoader $loader
Loader\YamlFileLoader $loader,
): void {
if (0 === \count($crudConfig)) {
return;
@@ -351,10 +359,81 @@ class ChillMainExtension extends Extension implements
// Note: the controller are loaded inside compiler pass
}
/**
* This method prepend framework configuration with legacy configuration from "ovhCloudTransporter".
*
* It can be safely removed when the option chill_main.short_message.dsn will be removed.
*/
private function prependNotifierTexterWithLegacyData(ContainerBuilder $container): void
{
foreach (array_reverse($container->getExtensionConfig('framework')) as $c) {
// we look into each configuration for framework. If there is a configuration for
// texter_transports in one of them, we don't configure anything else
if (null !== $notifConfig = $c['notifier'] ?? null) {
if (null !== ($notifConfig['texter_transports'] ?? null)) {
return;
}
}
}
// there is no texter config, we try to configure one
$configs = $container->getExtensionConfig('chill_main');
$notifierSet = false;
foreach (array_reverse($configs) as $config) {
if (!array_key_exists('short_messages', $config)) {
continue;
}
if (array_key_exists('dsn', $config['short_messages'])) {
$container->prependExtensionConfig('framework', [
'notifier' => [
'texter_transports' => [
'ovh_legacy' => $config['short_messages']['dsn'],
],
],
]);
$notifierSet = true;
}
}
if (!$notifierSet) {
$container->prependExtensionConfig('framework', [
'notifier' => [
'texter_transports' => [
'dummy' => 'null://null',
],
],
]);
}
}
protected function prependCruds(ContainerBuilder $container)
{
$container->prependExtensionConfig('chill_main', [
'cruds' => [
[
'class' => UserGroup::class,
'controller' => UserGroupAdminController::class,
'name' => 'admin_user_group',
'base_path' => '/admin/main/user-group',
'base_role' => 'ROLE_ADMIN',
'form_class' => UserGroupType::class,
'actions' => [
'index' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/UserGroup/index.html.twig',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/UserGroup/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/UserGroup/edit.html.twig',
],
],
],
[
'class' => UserJob::class,
'controller' => UserJobController::class,
@@ -487,6 +566,28 @@ class ChillMainExtension extends Extension implements
],
],
],
[
'class' => Gender::class,
'name' => 'main_gender',
'base_path' => '/admin/main/gender',
'base_role' => 'ROLE_ADMIN',
'form_class' => GenderType::class,
'controller' => GenderController::class,
'actions' => [
'index' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/Gender/index.html.twig',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/Gender/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/Gender/edit.html.twig',
],
],
],
[
'class' => Language::class,
'name' => 'main_language',
@@ -790,6 +891,21 @@ class ChillMainExtension extends Extension implements
],
],
],
[
'class' => Gender::class,
'name' => 'gender',
'base_path' => '/api/1.0/main/gender',
'base_role' => 'ROLE_USER',
'controller' => GenderApiController::class,
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
],
],
[
'class' => GeographicalUnitLayer::class,
'controller' => GeographicalUnitApiController::class,

View File

@@ -44,7 +44,7 @@ class ExportsCompilerPass implements CompilerPassInterface
private function compileExportElementsProvider(
Definition $chillManagerDefinition,
ContainerBuilder $container
ContainerBuilder $container,
) {
$taggedServices = $container->findTaggedServiceIds(
'chill.export_elements_provider'
@@ -73,7 +73,7 @@ class ExportsCompilerPass implements CompilerPassInterface
private function compileFormatters(
Definition $chillManagerDefinition,
ContainerBuilder $container
ContainerBuilder $container,
) {
$taggedServices = $container->findTaggedServiceIds(
'chill.export_formatter'

View File

@@ -1,91 +0,0 @@
<?php
/**
* Chill is a software for social workers.
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
/*
* 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\MainBundle\DependencyInjection\CompilerPass;
use Chill\MainBundle\Service\ShortMessage\NullShortMessageSender;
use Chill\MainBundle\Service\ShortMessage\ShortMessageTransporter;
use Chill\MainBundle\Service\ShortMessageOvh\OvhShortMessageSender;
use libphonenumber\PhoneNumberUtil;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
class ShortMessageCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$config = $container->resolveEnvPlaceholders($container->getParameter('chill_main.short_messages'), true);
// weird fix for special characters
$config['dsn'] = str_replace(['%%'], ['%'], (string) $config['dsn']);
$dsn = parse_url($config['dsn']);
parse_str($dsn['query'] ?? '', $dsn['queries']);
if ('null' === $dsn['scheme'] || false === $config['enabled']) {
$defaultTransporter = new Reference(NullShortMessageSender::class);
} elseif ('ovh' === $dsn['scheme']) {
if (!class_exists('\\'.\Ovh\Api::class)) {
throw new RuntimeException('Class \\Ovh\\Api not found');
}
foreach (['user', 'host', 'pass'] as $component) {
if (!\array_key_exists($component, $dsn)) {
throw new RuntimeException(sprintf('The component %s does not exist in dsn. Please provide a dsn like ovh://applicationKey:applicationSecret@endpoint?consumerKey=xxxx&sender=yyyy&service_name=zzzz', $component));
}
$container->setParameter('chill_main.short_messages.ovh_config_'.$component, $dsn[$component]);
}
foreach (['consumer_key', 'sender', 'service_name'] as $param) {
if (!\array_key_exists($param, $dsn['queries'])) {
throw new RuntimeException(sprintf('The parameter %s does not exist in dsn. Please provide a dsn like ovh://applicationKey:applicationSecret@endpoint?consumerKey=xxxx&sender=yyyy&service_name=zzzz', $param));
}
$container->setParameter('chill_main.short_messages.ovh_config_'.$param, $dsn['queries'][$param]);
}
$ovh = new Definition();
$ovh
->setClass('\\'.\Ovh\Api::class)
->setArgument(0, $dsn['user'])
->setArgument(1, $dsn['pass'])
->setArgument(2, $dsn['host'])
->setArgument(3, $dsn['queries']['consumer_key']);
$container->setDefinition(\Ovh\Api::class, $ovh);
$ovhSender = new Definition();
$ovhSender
->setClass(OvhShortMessageSender::class)
->setArgument(0, new Reference(\Ovh\Api::class))
->setArgument(1, $dsn['queries']['service_name'])
->setArgument(2, $dsn['queries']['sender'])
->setArgument(3, new Reference(LoggerInterface::class))
->setArgument(4, new Reference(PhoneNumberUtil::class));
$container->setDefinition(OvhShortMessageSender::class, $ovhSender);
$defaultTransporter = new Reference(OvhShortMessageSender::class);
} else {
throw new RuntimeException(sprintf('Cannot find a sender for this dsn: %s', $config['dsn']));
}
$container->getDefinition(ShortMessageTransporter::class)
->setArgument(0, $defaultTransporter);
}
}

View File

@@ -26,7 +26,7 @@ class Configuration implements ConfigurationInterface
public function __construct(
array $widgetFactories,
private readonly ContainerBuilder $containerBuilder
private readonly ContainerBuilder $containerBuilder,
) {
$this->setWidgetFactories($widgetFactories);
}
@@ -85,6 +85,29 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->end() // end of notifications
->arrayNode('workflow_signature')
->children()
->arrayNode('base_signer')
->children()
->arrayNode('document_kinds')
->arrayPrototype()
->children()
->scalarNode('key')->cannotBeEmpty()->end()
->arrayNode('labels')
->arrayPrototype()
->children()
->scalarNode('lang')->cannotBeEmpty()->end()
->scalarNode('label')->cannotBeEmpty()->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end() // end of workflow signature document types
->arrayNode('phone_helper')
->canBeUnset()
->children()
@@ -100,6 +123,7 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->arrayNode('short_messages')
->setDeprecated('chill-project/chill-bundles', '3.7.0', 'Since 3.7.0, Chill use the Notifier component to send message. Configure the notifier instead. In the meantime, the previous available OVH configuration will be append to the notifier component.')
->canBeEnabled()
->children()
->scalarNode('dsn')->cannotBeEmpty()->defaultValue('null://null')

View File

@@ -122,7 +122,7 @@ abstract class AbstractWidgetsCompilerPass implements CompilerPassInterface
public function doProcess(
ContainerBuilder $container,
$extension,
$containerWidgetConfigParameterName
$containerWidgetConfigParameterName,
) {
if (!$container->hasDefinition(self::WIDGET_MANAGER)) {
throw new \LogicException('the service '.self::WIDGET_MANAGER.' should be present. It is required by '.self::class);
@@ -230,7 +230,7 @@ abstract class AbstractWidgetsCompilerPass implements CompilerPassInterface
// check the alias does not exists yet
if (\array_key_exists($attr[self::WIDGET_SERVICE_TAG_ALIAS], $this->widgetServices)) {
throw new InvalidArgumentException('a service has already be defined with the '.self::WIDGET_SERVICE_TAG_ALIAS.' '.$attr[self::WIDGET_SERVICE_TAG_ALIAS]);
throw new InvalidConfigurationException('a service has already be defined with the '.self::WIDGET_SERVICE_TAG_ALIAS.' '.$attr[self::WIDGET_SERVICE_TAG_ALIAS]);
}
// register the service as available
@@ -259,7 +259,7 @@ abstract class AbstractWidgetsCompilerPass implements CompilerPassInterface
// check the alias does not exists yet
if (\array_key_exists($alias, $this->widgetServices)) {
throw new InvalidArgumentException('a service has already be defined with the '.self::WIDGET_SERVICE_TAG_ALIAS.' '.$alias);
throw new InvalidConfigurationException('a service has already be defined with the '.self::WIDGET_SERVICE_TAG_ALIAS.' '.$alias);
}
// register the factory as available
@@ -280,7 +280,7 @@ abstract class AbstractWidgetsCompilerPass implements CompilerPassInterface
WidgetFactoryInterface $factory,
$place,
$order,
array $config
array $config,
) {
$serviceId = $factory->getServiceId($container, $place, $order, $config);
$definition = $factory->createDefinition(

View File

@@ -92,7 +92,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface
* This list is computed by a materialized view. It won't be populated until a refresh is done
* on the materialized view.
*
* @var Collection<GeographicalUnit>
* @var Collection<int, GeographicalUnit>
*
* @readonly
*/
@@ -446,7 +446,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
public function setLinkedToThirdParty($linkedToThirdParty): self
public function setLinkedToThirdParty(?ThirdParty $linkedToThirdParty): self
{
$this->linkedToThirdParty = $linkedToThirdParty;

View File

@@ -21,9 +21,9 @@ use Symfony\Component\Serializer\Annotation as Serializer;
class Center implements HasCenterInterface, \Stringable
{
/**
* @var Collection<GroupCenter>
* @var Collection<int, GroupCenter>
*/
#[ORM\OneToMany(targetEntity: GroupCenter::class, mappedBy: 'center')]
#[ORM\OneToMany(mappedBy: 'center', targetEntity: GroupCenter::class)]
private Collection $groupCenters;
#[Serializer\Groups(['docgen:read'])]
@@ -36,11 +36,11 @@ class Center implements HasCenterInterface, \Stringable
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
private string $name = '';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
private bool $isActive = true;
/**
* @var Collection<Regroupment>
* @var Collection<int, Regroupment>
*/
#[ORM\ManyToMany(targetEntity: Regroupment::class, mappedBy: 'centers')]
private Collection $regroupments;
@@ -111,7 +111,7 @@ class Center implements HasCenterInterface, \Stringable
/**
* @return $this
*/
public function setName($name)
public function setName(string $name)
{
$this->name = $name;

View File

@@ -39,7 +39,7 @@ class Country
* @var array<string, string>
*/
#[Groups(['read', 'docgen:read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
#[Context(['is-translatable' => true], groups: ['docgen:read'])]
private array $name = [];

View File

@@ -30,13 +30,13 @@ class CronJobExecution
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
private ?int $lastStatus = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => "'{}'::jsonb", 'jsonb' => true])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '{}', 'jsonb' => true])]
private array $lastExecutionData = [];
public function __construct(
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false)]
#[ORM\Id]
private string $key
private string $key,
) {
$this->lastStart = new \DateTimeImmutable('now');
}

View File

@@ -39,7 +39,7 @@ class DashboardConfigItem
private ?User $user = null;
#[Serializer\Groups(['dashboardConfigItem:read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '{}', 'jsonb' => true])]
private array $metadata = [];
public function getId(): ?int

View File

@@ -61,10 +61,7 @@ class CommentEmbeddable
$this->date = $date;
}
/**
* @param int $userId
*/
public function setUserId($userId)
public function setUserId(?int $userId)
{
$this->userId = $userId;
}

View File

@@ -57,7 +57,7 @@ class PrivateCommentEmbeddable
return $this;
}
public function setComments($comments)
public function setComments(array $comments)
{
$this->comments = $comments;

View File

@@ -0,0 +1,104 @@
<?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\MainBundle\Entity;
use Chill\MainBundle\Repository\GenderRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['chill_main_gender' => Gender::class])]
#[ORM\Entity(repositoryClass: GenderRepository::class)]
#[ORM\Table(name: 'chill_main_gender')]
class Gender
{
#[Serializer\Groups(['read', 'docgen:read'])]
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
#[Serializer\Groups(['read', 'docgen:read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
private array $label = [];
#[Serializer\Groups(['read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
private bool $active = true;
#[Assert\NotNull(message: 'You must choose a gender translation')]
#[Serializer\Groups(['read', 'docgen:read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, enumType: GenderEnum::class)]
private GenderEnum $genderTranslation;
#[Serializer\Groups(['read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, enumType: GenderIconEnum::class)]
private GenderIconEnum $icon;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::FLOAT, name: 'ordering', nullable: true, options: ['default' => '0.0'])]
private float $order = 0;
public function getId(): int
{
return $this->id;
}
public function getLabel(): array
{
return $this->label;
}
public function setLabel(array $label): void
{
$this->label = $label;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): void
{
$this->active = $active;
}
public function getGenderTranslation(): GenderEnum
{
return $this->genderTranslation;
}
public function setGenderTranslation(GenderEnum $genderTranslation): void
{
$this->genderTranslation = $genderTranslation;
}
public function getIcon(): GenderIconEnum
{
return $this->icon;
}
public function setIcon(GenderIconEnum $icon): void
{
$this->icon = $icon;
}
public function getOrder(): float
{
return $this->order;
}
public function setOrder(float $order): void
{
$this->order = $order;
}
}

View File

@@ -0,0 +1,20 @@
<?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\MainBundle\Entity;
enum GenderEnum: string
{
case MALE = 'man';
case FEMALE = 'woman';
case NEUTRAL = 'neutral';
case UNKNOWN = 'unknown';
}

View File

@@ -0,0 +1,22 @@
<?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\MainBundle\Entity;
enum GenderIconEnum: string
{
case MALE = 'bi bi-gender-male';
case FEMALE = 'bi bi-gender-female';
case NEUTRAL = 'bi bi-gender-neuter';
case AMBIGUOUS = 'bi bi-gender-ambiguous';
case TRANS = 'bi bi-gender-trans';
case UNKNOWN = 'bi bi-question';
}

View File

@@ -48,6 +48,6 @@ class SimpleGeographicalUnitDTO
* @psalm-readonly
*/
#[Serializer\Groups(['read'])]
public int $layerId
public int $layerId,
) {}
}

View File

@@ -36,9 +36,9 @@ class GeographicalUnitLayer
private string $refId = '';
/**
* @var Collection<GeographicalUnit>
* @var Collection<int, GeographicalUnit>
*/
#[ORM\OneToMany(targetEntity: GeographicalUnit::class, mappedBy: 'layer')]
#[ORM\OneToMany(mappedBy: 'layer', targetEntity: GeographicalUnit::class)]
private Collection $units;
public function __construct()

View File

@@ -34,7 +34,7 @@ class GroupCenter
private ?PermissionsGroup $permissionsGroup = null;
/**
* @var Collection<User::class>
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'groupCenters')]
private Collection $users;

View File

@@ -59,11 +59,9 @@ class Language
/**
* Set id.
*
* @param string $id
*
* @return Language
*/
public function setId($id)
public function setId(?string $id)
{
$this->id = $id;
@@ -77,7 +75,7 @@ class Language
*
* @return Language
*/
public function setName($name)
public function setName(array $name)
{
$this->name = $name;

View File

@@ -26,7 +26,7 @@ use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
class Location implements TrackCreationInterface, TrackUpdateInterface
{
#[Serializer\Groups(['read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: true)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
private bool $active = true;
#[Serializer\Groups(['read', 'write', 'docgen:read'])]

View File

@@ -34,7 +34,7 @@ class LocationType
final public const STATUS_REQUIRED = 'required';
#[Serializer\Groups(['read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: true)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
private bool $active = true;
#[Serializer\Groups(['read'])]
@@ -54,7 +54,7 @@ class LocationType
private ?string $defaultFor = null;
#[Serializer\Groups(['read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
private bool $editableByUsers = true;
#[Serializer\Groups(['read', 'docgen:read'])]

View File

@@ -30,7 +30,7 @@ class Notification implements TrackUpdateInterface
private array $addedAddresses = [];
/**
* @var Collection<User>
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
@@ -41,7 +41,7 @@ class Notification implements TrackUpdateInterface
*
* @var array|string[]
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
private array $addressesEmails = [];
/**
@@ -54,9 +54,9 @@ class Notification implements TrackUpdateInterface
private ?ArrayCollection $addressesOnLoad = null;
/**
* @var Collection<NotificationComment>
* @var Collection<int, NotificationComment>
*/
#[ORM\OneToMany(targetEntity: NotificationComment::class, mappedBy: 'notification', orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'notification', targetEntity: NotificationComment::class, orphanRemoval: true)]
#[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
private Collection $comments;
@@ -88,7 +88,7 @@ class Notification implements TrackUpdateInterface
private string $title = '';
/**
* @var Collection<User>
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]

View File

@@ -24,13 +24,13 @@ class PermissionsGroup
/**
* @var string[]
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
private array $flags = [];
/**
* @var Collection<GroupCenter>
* @var Collection<int, GroupCenter>
*/
#[ORM\OneToMany(targetEntity: GroupCenter::class, mappedBy: 'permissionsGroup')]
#[ORM\OneToMany(mappedBy: 'permissionsGroup', targetEntity: GroupCenter::class)]
private Collection $groupCenters;
#[ORM\Id]
@@ -42,7 +42,7 @@ class PermissionsGroup
private string $name = '';
/**
* @var Collection<RoleScope>
* @var Collection<int, RoleScope>
*/
#[ORM\ManyToMany(targetEntity: RoleScope::class, inversedBy: 'permissionsGroups', cascade: ['persist'])]
#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
@@ -137,7 +137,7 @@ class PermissionsGroup
/**
* @return $this
*/
public function setName($name)
public function setName(string $name)
{
$this->name = $name;

View File

@@ -157,11 +157,9 @@ class PostalCode implements TrackUpdateInterface, TrackCreationInterface
/**
* Set code.
*
* @param string $code
*
* @return PostalCode
*/
public function setCode($code)
public function setCode(?string $code)
{
$this->code = $code;
@@ -183,11 +181,9 @@ class PostalCode implements TrackUpdateInterface, TrackCreationInterface
/**
* Set name.
*
* @param string $name
*
* @return PostalCode
*/
public function setName($name)
public function setName(?string $name)
{
$this->name = $name;
@@ -197,11 +193,9 @@ class PostalCode implements TrackUpdateInterface, TrackCreationInterface
/**
* Set origin.
*
* @param int $origin
*
* @return PostalCode
*/
public function setOrigin($origin)
public function setOrigin(int $origin)
{
$this->origin = $origin;

View File

@@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM;
class Regroupment
{
/**
* @var Collection<Center>
* @var Collection<int, Center>
*/
#[ORM\ManyToMany(targetEntity: Center::class, inversedBy: 'regroupments')]
#[ORM\Id]

View File

@@ -26,7 +26,7 @@ class RoleScope
private ?int $id = null;
/**
* @var Collection<PermissionsGroup>
* @var Collection<int, PermissionsGroup>
*/
#[ORM\ManyToMany(targetEntity: PermissionsGroup::class, mappedBy: 'roleScopes')]
private Collection $permissionsGroups;

View File

@@ -42,9 +42,9 @@ class Scope
private array $name = [];
/**
* @var Collection<RoleScope>
* @var Collection<int, RoleScope>
*/
#[ORM\OneToMany(targetEntity: RoleScope::class, mappedBy: 'scope')]
#[ORM\OneToMany(mappedBy: 'scope', targetEntity: RoleScope::class)]
#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
private Collection $roleScopes;

View File

@@ -45,7 +45,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
/**
* Array where SAML attributes's data are stored.
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
private array $attributes = [];
#[ORM\ManyToOne(targetEntity: Civility::class)]
@@ -64,7 +64,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
private bool $enabled = true;
/**
* @var Collection<GroupCenter>
* @var Collection<int, GroupCenter>
*/
#[ORM\ManyToMany(targetEntity: GroupCenter::class, inversedBy: 'users')]
#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
@@ -83,9 +83,9 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
private ?Location $mainLocation = null;
/**
* @var Collection&Selectable<int, UserScopeHistory>
* @var Collection<int, UserScopeHistory>&Selectable
*/
#[ORM\OneToMany(targetEntity: UserScopeHistory::class, mappedBy: 'user', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserScopeHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
private Collection&Selectable $scopeHistories;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
@@ -98,9 +98,9 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
private ?string $salt = null;
/**
* @var Collection&Selectable<int, UserJobHistory>
* @var Collection<int, UserJobHistory>&Selectable
*/
#[ORM\OneToMany(targetEntity: UserJobHistory::class, mappedBy: 'user', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserJobHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
private Collection&Selectable $jobHistories;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 80)]
@@ -216,13 +216,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->mainLocation;
}
public function getMainScope(?\DateTimeImmutable $at = null): ?Scope
public function getMainScope(?\DateTimeImmutable $atDate = null): ?Scope
{
$at ??= new \DateTimeImmutable('now');
$atDate ??= new \DateTimeImmutable('now');
foreach ($this->scopeHistories as $scopeHistory) {
if ($at >= $scopeHistory->getStartDate() && (
null === $scopeHistory->getEndDate() || $at < $scopeHistory->getEndDate()
if ($atDate >= $scopeHistory->getStartDate() && (
null === $scopeHistory->getEndDate() || $atDate < $scopeHistory->getEndDate()
)) {
return $scopeHistory->getScope();
}
@@ -265,13 +265,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->salt;
}
public function getUserJob(?\DateTimeImmutable $at = null): ?UserJob
public function getUserJob(?\DateTimeImmutable $atDate = null): ?UserJob
{
$at ??= new \DateTimeImmutable('now');
$atDate ??= new \DateTimeImmutable('now');
foreach ($this->jobHistories as $jobHistory) {
if ($at >= $jobHistory->getStartDate() && (
null === $jobHistory->getEndDate() || $at < $jobHistory->getEndDate()
if ($atDate >= $jobHistory->getStartDate() && (
null === $jobHistory->getEndDate() || $atDate < $jobHistory->getEndDate()
)) {
return $jobHistory->getJob();
}
@@ -285,6 +285,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->jobHistories;
}
public function getUserScopeHistories(): Collection
{
return $this->scopeHistories;
}
/**
* @return ArrayCollection|UserJobHistory[]
*/
@@ -432,7 +437,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
/**
* @return $this
*/
public function setEmail($email)
public function setEmail(?string $email)
{
$this->email = $email;
@@ -442,7 +447,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
/**
* @return $this
*/
public function setEmailCanonical($emailCanonical)
public function setEmailCanonical(?string $emailCanonical)
{
$this->emailCanonical = $emailCanonical;
@@ -516,7 +521,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
/**
* @return $this
*/
public function setPassword($password)
public function setPassword(string $password)
{
$this->password = $password;
@@ -526,7 +531,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
/**
* @return $this
*/
public function setSalt($salt)
public function setSalt(?string $salt)
{
$this->salt = $salt;
@@ -588,7 +593,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
/**
* @return $this
*/
public function setUsernameCanonical($usernameCanonical)
public function setUsernameCanonical(?string $usernameCanonical)
{
$this->usernameCanonical = $usernameCanonical;

View File

@@ -13,37 +13,49 @@ namespace Chill\MainBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ORM\Table(name: 'chill_main_user_group')]
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['user_group' => UserGroup::class])]
// this discriminator key is required for automated denormalization
#[DiscriminatorMap('type', mapping: ['user_group' => UserGroup::class])]
class UserGroup
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
#[Serializer\Groups(['read'])]
private ?int $id = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
private bool $active = true;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
#[Serializer\Groups(['read'])]
private array $label = [];
/**
* @var \Doctrine\Common\Collections\Collection<int, \Chill\MainBundle\Entity\User>
* @var Collection<int, User>&Selectable<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_user_group_user')]
private Collection $users;
private Collection&Selectable $users;
/**
* @var Collection<int, User>&Selectable<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_user_group_user_admin')]
private Collection&Selectable $adminUsers;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#ffffffff'])]
#[Serializer\Groups(['read'])]
private string $backgroundColor = '#ffffffff';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#000000ff'])]
#[Serializer\Groups(['read'])]
private string $foregroundColor = '#000000ff';
/**
@@ -53,14 +65,46 @@ class UserGroup
* An empty string means "no exclusion"
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
#[Serializer\Groups(['read'])]
private string $excludeKey = '';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
#[Assert\Email]
private string $email = '';
public function __construct()
{
$this->adminUsers = new ArrayCollection();
$this->users = new ArrayCollection();
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): self
{
$this->active = $active;
return $this;
}
public function addAdminUser(User $user): self
{
if (!$this->adminUsers->contains($user)) {
$this->adminUsers[] = $user;
}
return $this;
}
public function removeAdminUser(User $user): self
{
$this->adminUsers->removeElement($user);
return $this;
}
public function addUser(User $user): self
{
if (!$this->users->contains($user)) {
@@ -89,11 +133,22 @@ class UserGroup
return $this->label;
}
public function getUsers(): Collection
/**
* @return Selectable<int, User>&Collection<int, User>
*/
public function getUsers(): Collection&Selectable
{
return $this->users;
}
/**
* @return Selectable<int, User>&Collection<int, User>
*/
public function getAdminUsers(): Collection&Selectable
{
return $this->adminUsers;
}
public function getForegroundColor(): string
{
return $this->foregroundColor;
@@ -137,6 +192,23 @@ class UserGroup
return $this;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function hasEmail(): bool
{
return '' !== $this->email;
}
/**
* Checks if the current object is an instance of the UserGroup class.
*
@@ -148,4 +220,25 @@ class UserGroup
{
return true;
}
public function contains(User $user): bool
{
return $this->users->contains($user);
}
public function getUserListByLabelAscending(): ReadableCollection
{
$criteria = Criteria::create();
$criteria->orderBy(['label' => Order::Ascending]);
return $this->getUsers()->matching($criteria);
}
public function getAdminUserListByLabelAscending(): ReadableCollection
{
$criteria = Criteria::create();
$criteria->orderBy(['label' => Order::Ascending]);
return $this->getAdminUsers()->matching($criteria);
}
}

View File

@@ -17,9 +17,10 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Order;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
@@ -35,38 +36,9 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
use TrackUpdateTrait;
/**
* a list of future cc users for the next steps.
*
* @var array|User[]
* @var Collection<int, EntityWorkflowComment>
*/
public array $futureCcUsers = [];
/**
* a list of future dest emails for the next steps.
*
* This is in used in order to let controller inform who will be the future emails which will validate
* the next step. This is necessary to perform some computation about the next emails, before they are
* associated to the entity EntityWorkflowStep.
*
* @var array|string[]
*/
public array $futureDestEmails = [];
/**
* a list of future dest users for the next steps.
*
* This is in used in order to let controller inform who will be the future users which will validate
* the next step. This is necessary to perform some computation about the next users, before they are
* associated to the entity EntityWorkflowStep.
*
* @var array|User[]
*/
public array $futureDestUsers = [];
/**
* @var Collection<EntityWorkflowComment>
*/
#[ORM\OneToMany(targetEntity: EntityWorkflowComment::class, mappedBy: 'entityWorkflow', orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowComment::class, orphanRemoval: true)]
private Collection $comments;
#[ORM\Id]
@@ -81,12 +53,12 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
private int $relatedEntityId;
/**
* @var Collection<EntityWorkflowStep>
* @var Collection<int, EntityWorkflowStep>&Selectable<int, EntityWorkflowStep>
*/
#[Assert\Valid(traverse: true)]
#[ORM\OneToMany(targetEntity: EntityWorkflowStep::class, mappedBy: 'entityWorkflow', orphanRemoval: true, cascade: ['persist'])]
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowStep::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['transitionAt' => \Doctrine\Common\Collections\Criteria::ASC, 'id' => 'ASC'])]
private Collection $steps;
private Collection&Selectable $steps;
/**
* @var array|EntityWorkflowStep[]|null
@@ -94,14 +66,14 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
private ?array $stepsChainedCache = null;
/**
* @var Collection<User>
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_final')]
private Collection $subscriberToFinal;
/**
* @var Collection<User>
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_step')]
@@ -115,12 +87,19 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
private string $workflowName;
/**
* @var Collection<int, EntityWorkflowAttachment>
*/
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowAttachment::class, cascade: ['remove'], orphanRemoval: true)]
private Collection $attachments;
public function __construct()
{
$this->subscriberToFinal = new ArrayCollection();
$this->subscriberToStep = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->steps = new ArrayCollection();
$this->attachments = new ArrayCollection();
$initialStep = new EntityWorkflowStep();
$initialStep
@@ -170,6 +149,35 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
/**
* @return $this
*
* @internal use @{EntityWorkflowAttachement::__construct} instead
*/
public function addAttachment(EntityWorkflowAttachment $attachment): self
{
if (!$this->attachments->contains($attachment)) {
$this->attachments[] = $attachment;
}
return $this;
}
/**
* @return Collection<int, EntityWorkflowAttachment>
*/
public function getAttachments(): Collection
{
return $this->attachments;
}
public function removeAttachment(EntityWorkflowAttachment $attachment): self
{
$this->attachments->removeElement($attachment);
return $this;
}
public function getComments(): Collection
{
return $this->comments;
@@ -271,17 +279,24 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
throw new \RuntimeException();
}
public function getSteps(): ArrayCollection|Collection
/**
* @return Selectable<int, EntityWorkflowStep>&Collection<int, EntityWorkflowStep>
*/
public function getSteps(): Collection&Selectable
{
return $this->steps;
}
/**
* @throws \Exception
*/
public function getStepsChained(): array
{
if (\is_array($this->stepsChainedCache)) {
return $this->stepsChainedCache;
}
/** @var \ArrayIterator $iterator */
$iterator = $this->steps->getIterator();
$current = null;
$steps = [];
@@ -342,7 +357,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
}
}
return $usersInvolved;
return array_values($usersInvolved);
}
public function getWorkflowName(): string
@@ -363,8 +378,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
public function isFreeze(): bool
{
$steps = $this->getStepsChained();
foreach ($this->getStepsChained() as $step) {
if ($step->isFreezeAfter()) {
return true;
@@ -374,6 +387,22 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return false;
}
public function isOnHoldByUser(User $user): bool
{
return $this->getCurrentStep()->isOnHoldByUser($user);
}
public function isUserInvolved(User $user): bool
{
foreach ($this->getSteps() as $step) {
if ($step->getAllDestUser()->contains($user)) {
return true;
}
}
return false;
}
public function isUserSubscribedToFinal(User $user): bool
{
return $this->subscriberToFinal->contains($user);
@@ -438,15 +467,59 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
}
/**
* Method use by marking store.
* Method used by marking store.
*
* @return $this
*/
public function setStep(string $step): self
{
public function setStep(
string $step,
WorkflowTransitionContextDTO $transitionContextDTO,
string $transition,
\DateTimeImmutable $transitionAt,
?User $byUser = null,
): self {
$previousStep = $this->getCurrentStep();
$previousStep
->setComment($transitionContextDTO->comment)
->setTransitionAfter($transition)
->setTransitionAt($transitionAt)
->setTransitionBy($byUser);
$newStep = new EntityWorkflowStep();
$newStep->setCurrentStep($step);
foreach ($transitionContextDTO->futureCcUsers as $user) {
$newStep->addCcUser($user);
}
foreach ($transitionContextDTO->getFutureDestUsers() as $user) {
$newStep->addDestUser($user);
}
foreach ($transitionContextDTO->getFutureDestUserGroups() as $userGroup) {
$newStep->addDestUserGroup($userGroup);
}
if (null !== $transitionContextDTO->futureUserSignature) {
$newStep->addDestUser($transitionContextDTO->futureUserSignature);
}
if (null !== $transitionContextDTO->futureUserSignature) {
new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature);
} else {
foreach ($transitionContextDTO->futurePersonSignatures as $personSignature) {
new EntityWorkflowStepSignature($newStep, $personSignature);
}
}
foreach ($transitionContextDTO->futureDestineeThirdParties as $thirdParty) {
new EntityWorkflowSend($newStep, $thirdParty, $transitionAt->add(new \DateInterval('P30D')));
}
foreach ($transitionContextDTO->futureDestineeEmails as $email) {
new EntityWorkflowSend($newStep, $email, $transitionAt->add(new \DateInterval('P30D')));
}
// copy the freeze
if ($this->isFreeze()) {
$newStep->setFreezeAfter(true);
@@ -472,4 +545,41 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return $this->steps->get($this->steps->count() - 2);
}
public function isOnHoldAtCurrentStep(): bool
{
return $this->getCurrentStep()->getHoldsOnStep()->count() > 0;
}
/**
* Determines if the workflow has become stale after a given date.
*
* This function checks the creation date and the transition states of the workflow steps.
* A workflow is considered stale if:
* - The creation date is before the given date and no transitions have occurred since the creation.
* - Or if there are no transitions after the given date.
*
* @param \DateTimeImmutable $at the date to compare against the workflow's status
*
* @return bool true if the workflow is stale after the given date, false otherwise
*/
public function isStaledAt(\DateTimeImmutable $at): bool
{
// if there is no transition since the creation, then the workflow is staled
if ('initial' === $this->getCurrentStep()->getCurrentStep()
&& null === $this->getCurrentStep()->getTransitionAt()
) {
if (null === $this->getCreatedAt()) {
return false;
}
if ($this->getCreatedAt() < $at) {
return true;
}
return false;
}
return $this->getCurrentStepChained()->getPrevious()->getTransitionAt() < $at;
}
}

View File

@@ -0,0 +1,80 @@
<?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\MainBundle\Entity\Workflow;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity()]
#[ORM\Table(name: 'chill_main_workflow_entity_attachment')]
#[ORM\UniqueConstraint(name: 'unique_generic_doc_by_workflow', columns: ['relatedGenericDocKey', 'relatedGenericDocIdentifiers', 'entityworkflow_id'])]
class EntityWorkflowAttachment implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
public function __construct(
#[ORM\Column(name: 'relatedGenericDocKey', type: Types::STRING, length: 255, nullable: false)]
private string $relatedGenericDocKey,
#[ORM\Column(name: 'relatedGenericDocIdentifiers', type: Types::JSON, nullable: false, options: ['jsonb' => true])]
private array $relatedGenericDocIdentifiers,
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'attachments')]
#[ORM\JoinColumn(nullable: false, name: 'entityworkflow_id')]
private EntityWorkflow $entityWorkflow,
/**
* Stored object related to the generic doc.
*
* This is a story to keep track more easily to stored object
*/
#[ORM\ManyToOne(targetEntity: StoredObject::class)]
#[ORM\JoinColumn(nullable: false, name: 'storedobject_id')]
private StoredObject $proxyStoredObject,
) {
$this->entityWorkflow->addAttachment($this);
}
public function getId(): ?int
{
return $this->id;
}
public function getEntityWorkflow(): EntityWorkflow
{
return $this->entityWorkflow;
}
public function getRelatedGenericDocIdentifiers(): array
{
return $this->relatedGenericDocIdentifiers;
}
public function getRelatedGenericDocKey(): string
{
return $this->relatedGenericDocKey;
}
public function getProxyStoredObject(): StoredObject
{
return $this->proxyStoredObject;
}
}

View File

@@ -17,6 +17,11 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Doctrine\ORM\Mapping as ORM;
/**
* Contains comment for entity workflow.
*
* **NOTE**: for now, this class is not in used. Comments are, for now, stored in the EntityWorkflowStep.
*/
#[ORM\Entity]
#[ORM\Table('chill_main_workflow_entity_comment')]
class EntityWorkflowComment implements TrackCreationInterface, TrackUpdateInterface

View File

@@ -0,0 +1,227 @@
<?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\MainBundle\Entity\Workflow;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Random\Randomizer;
/**
* An entity which stores then sending of a workflow's content to
* some external entity.
*/
#[ORM\Entity]
#[ORM\Table(name: 'chill_main_workflow_entity_send')]
class EntityWorkflowSend implements TrackCreationInterface
{
use TrackCreationTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: ThirdParty::class)]
#[ORM\JoinColumn(nullable: true)]
private ?ThirdParty $destineeThirdParty = null;
#[ORM\Column(type: Types::TEXT, nullable: false, options: ['default' => ''])]
private string $destineeEmail = '';
#[ORM\Column(type: 'uuid', unique: true, nullable: false)]
private UuidInterface $uuid;
#[ORM\Column(type: Types::STRING, length: 255, nullable: false)]
private string $privateToken;
#[ORM\Column(type: Types::INTEGER, nullable: false, options: ['default' => 0])]
private int $numberOfErrorTrials = 0;
/**
* @var Collection<int, EntityWorkflowSendView>
*/
#[ORM\OneToMany(mappedBy: 'send', targetEntity: EntityWorkflowSendView::class, cascade: ['remove'])]
private Collection $views;
public function __construct(
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'sends')]
#[ORM\JoinColumn(nullable: false)]
private EntityWorkflowStep $entityWorkflowStep,
string|ThirdParty $destinee,
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
private \DateTimeImmutable $expireAt,
) {
$this->uuid = Uuid::uuid4();
$random = new Randomizer();
$this->privateToken = bin2hex($random->getBytes(48));
$this->entityWorkflowStep->addSend($this);
if ($destinee instanceof ThirdParty) {
$this->destineeThirdParty = $destinee;
} else {
$this->destineeEmail = $destinee;
}
$this->views = new ArrayCollection();
}
/**
* @internal use the @see{EntityWorkflowSendView}'s constructor instead
*/
public function addView(EntityWorkflowSendView $view): self
{
if (!$this->views->contains($view)) {
$this->views->add($view);
}
return $this;
}
public function getDestineeEmail(): string
{
return $this->destineeEmail;
}
public function getDestineeThirdParty(): ?ThirdParty
{
return $this->destineeThirdParty;
}
public function getId(): ?int
{
return $this->id;
}
public function getNumberOfErrorTrials(): int
{
return $this->numberOfErrorTrials;
}
public function getPrivateToken(): string
{
return $this->privateToken;
}
public function getEntityWorkflowStep(): EntityWorkflowStep
{
return $this->entityWorkflowStep;
}
public function getEntityWorkflowStepChained(): ?EntityWorkflowStep
{
foreach ($this->getEntityWorkflowStep()->getEntityWorkflow()->getStepsChained() as $step) {
if ($this->getEntityWorkflowStep() === $step) {
return $step;
}
}
return null;
}
public function getUuid(): UuidInterface
{
return $this->uuid;
}
public function getExpireAt(): \DateTimeImmutable
{
return $this->expireAt;
}
public function getViews(): Collection
{
return $this->views;
}
public function increaseErrorTrials(): void
{
$this->numberOfErrorTrials = $this->numberOfErrorTrials + 1;
}
public function getDestinee(): string|ThirdParty
{
if (null !== $this->getDestineeThirdParty()) {
return $this->getDestineeThirdParty();
}
return $this->getDestineeEmail();
}
/**
* Determines the kind of destinee based on whether the destinee is a thirdParty or an emailAddress.
*
* @return 'thirdParty'|'email' 'thirdParty' if the destinee is a third party, 'email' otherwise
*/
public function getDestineeKind(): string
{
if (null !== $this->getDestineeThirdParty()) {
return 'thirdParty';
}
return 'email';
}
public function isViewed(): bool
{
return $this->views->count() > 0;
}
public function isExpired(?\DateTimeImmutable $now = null): bool
{
return ($now ?? new \DateTimeImmutable('now')) >= $this->expireAt;
}
/**
* Retrieves the most recent view.
*
* @return EntityWorkflowSendView|null returns the last view or null if there are no views
*/
public function getLastView(): ?EntityWorkflowSendView
{
$last = null;
foreach ($this->views as $view) {
if (null === $last) {
$last = $view;
} else {
if ($view->getViewAt() > $last->getViewAt()) {
$last = $view;
}
}
}
return $last;
}
/**
* Retrieves an array of views grouped by their remote IP address.
*
* @return array<string, list<EntityWorkflowSendView>> an associative array where the keys are IP addresses and the values are arrays of views associated with those IPs
*/
public function getViewsByIp(): array
{
$views = [];
foreach ($this->getViews() as $view) {
$views[$view->getRemoteIp()][] = $view;
}
return $views;
}
}

View File

@@ -0,0 +1,60 @@
<?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\MainBundle\Entity\Workflow;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
/**
* Register the viewing action from an external destinee.
*/
#[ORM\Entity(readOnly: true)]
#[ORM\Table(name: 'chill_main_workflow_entity_send_views')]
class EntityWorkflowSendView
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
public function __construct(
#[ORM\ManyToOne(targetEntity: EntityWorkflowSend::class, inversedBy: 'views')]
#[ORM\JoinColumn(nullable: false)]
private EntityWorkflowSend $send,
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private \DateTimeInterface $viewAt,
#[ORM\Column(type: Types::TEXT)]
private string $remoteIp = '',
) {
$this->send->addView($this);
}
public function getId(): ?int
{
return $this->id;
}
public function getRemoteIp(): string
{
return $this->remoteIp;
}
public function getSend(): EntityWorkflowSend
{
return $this->send;
}
public function getViewAt(): \DateTimeInterface
{
return $this->viewAt;
}
}

View File

@@ -0,0 +1,20 @@
<?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\MainBundle\Entity\Workflow;
enum EntityWorkflowSignatureStateEnum: string
{
case PENDING = 'pending';
case SIGNED = 'signed';
case REJECTED = 'rejected';
case CANCELED = 'canceled';
}

View File

@@ -12,12 +12,22 @@ declare(strict_types=1);
namespace Chill\MainBundle\Entity\Workflow;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* A step for each EntityWorkflow.
*
* The step contains the history of position. The current one is the one which transitionAt or transitionAfter is NULL.
*
* The comments field is populated by the comment of the one who apply the transition, it means that the comment for the
* "next" step is stored in the EntityWorkflowStep in the previous step.
*
* DestUsers are the one added at the transition. DestUserByAccessKey are the users who obtained permission after having
* clicked on a link to get access (email notification to groups).
*/
#[ORM\Entity]
#[ORM\Table('chill_main_workflow_entity_step')]
class EntityWorkflowStep
@@ -26,7 +36,7 @@ class EntityWorkflowStep
private string $accessKey;
/**
* @var Collection<User>
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_cc_user')]
@@ -42,19 +52,32 @@ class EntityWorkflowStep
private array $destEmail = [];
/**
* @var Collection<User>
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
private Collection $destUser;
/**
* @var Collection<User>
* @var Collection<int, UserGroup>
*/
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_group')]
private Collection $destUserGroups;
/**
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')]
private Collection $destUserByAccessKey;
/**
* @var Collection <int, EntityWorkflowStepSignature>
*/
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
private Collection $signatures;
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
private ?EntityWorkflow $entityWorkflow = null;
@@ -66,6 +89,11 @@ class EntityWorkflowStep
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
/**
* If this is the final step.
*
* This property is filled by a listener.
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
private bool $isFinal = false;
@@ -92,11 +120,27 @@ class EntityWorkflowStep
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
private ?string $transitionByEmail = null;
/**
* @var Collection<int, EntityWorkflowStepHold>
*/
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class)]
private Collection $holdsOnStep;
/**
* @var Collection<int, EntityWorkflowSend>
*/
#[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
private Collection $sends;
public function __construct()
{
$this->ccUser = new ArrayCollection();
$this->destUser = new ArrayCollection();
$this->destUserGroups = new ArrayCollection();
$this->destUserByAccessKey = new ArrayCollection();
$this->signatures = new ArrayCollection();
$this->holdsOnStep = new ArrayCollection();
$this->sends = new ArrayCollection();
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
}
@@ -109,6 +153,9 @@ class EntityWorkflowStep
return $this;
}
/**
* @deprecated
*/
public function addDestEmail(string $email): self
{
if (!\in_array($email, $this->destEmail, true)) {
@@ -127,6 +174,22 @@ class EntityWorkflowStep
return $this;
}
public function addDestUserGroup(UserGroup $userGroup): self
{
if (!$this->destUserGroups->contains($userGroup)) {
$this->destUserGroups[] = $userGroup;
}
return $this;
}
public function removeDestUserGroup(UserGroup $userGroup): self
{
$this->destUserGroups->removeElement($userGroup);
return $this;
}
public function addDestUserByAccessKey(User $user): self
{
if (!$this->destUserByAccessKey->contains($user) && !$this->destUser->contains($user)) {
@@ -136,6 +199,39 @@ class EntityWorkflowStep
return $this;
}
/**
* @internal use @see{EntityWorkflowStepSignature}'s constructor instead
*/
public function addSignature(EntityWorkflowStepSignature $signature): self
{
if (!$this->signatures->contains($signature)) {
$this->signatures[] = $signature;
}
return $this;
}
/**
* @internal use @see{EntityWorkflowSend}'s constructor instead
*/
public function addSend(EntityWorkflowSend $send): self
{
if (!$this->sends->contains($send)) {
$this->sends[] = $send;
}
return $this;
}
public function removeSignature(EntityWorkflowStepSignature $signature): self
{
if ($this->signatures->contains($signature)) {
$this->signatures->removeElement($signature);
}
return $this;
}
public function getAccessKey(): string
{
return $this->accessKey;
@@ -143,7 +239,9 @@ class EntityWorkflowStep
/**
* get all the users which are allowed to apply a transition: those added manually, and
* those added automatically bu using an access key.
* those added automatically by using an access key.
*
* This method exclude the users associated with user groups
*
* @psalm-suppress DuplicateArrayKey
*/
@@ -157,11 +255,24 @@ class EntityWorkflowStep
);
}
/**
* @return Collection<int, UserGroup>
*/
public function getDestUserGroups(): Collection
{
return $this->destUserGroups;
}
public function getCcUser(): Collection
{
return $this->ccUser;
}
/**
* This is the comment from the one who apply the transition.
*
* It means that it must be saved when the user apply a transition.
*/
public function getComment(): string
{
return $this->comment;
@@ -172,6 +283,11 @@ class EntityWorkflowStep
return $this->currentStep;
}
/**
* @return array<string>
*
* @deprecated
*/
public function getDestEmail(): array
{
return $this->destEmail;
@@ -198,6 +314,22 @@ class EntityWorkflowStep
return $this->entityWorkflow;
}
/**
* @return Collection<int, EntityWorkflowStepSignature>
*/
public function getSignatures(): Collection
{
return $this->signatures;
}
/**
* @return Collection<int, EntityWorkflowSend>
*/
public function getSends(): Collection
{
return $this->sends;
}
public function getId(): ?int
{
return $this->id;
@@ -233,6 +365,9 @@ class EntityWorkflowStep
return $this->transitionByEmail;
}
/**
* @return bool true if this is the end of the EntityWorkflow
*/
public function isFinal(): bool
{
return $this->isFinal;
@@ -243,6 +378,20 @@ class EntityWorkflowStep
return $this->freezeAfter;
}
public function isOnHoldByUser(User $user): bool
{
foreach ($this->getHoldsOnStep() as $onHold) {
if ($onHold->getByUser() === $user) {
return true;
}
}
return false;
}
/**
* @return bool if the EntityWorkflowStep is waiting for a transition, and is not the final step
*/
public function isWaitingForTransition(): bool
{
if (null !== $this->transitionAfter) {
@@ -377,23 +526,17 @@ class EntityWorkflowStep
return $this;
}
#[Assert\Callback]
public function validateOnCreation(ExecutionContextInterface $context, mixed $payload): void
public function getHoldsOnStep(): Collection
{
return;
return $this->holdsOnStep;
}
if ($this->isFinalizeAfter()) {
if (0 !== \count($this->getDestUser())) {
$context->buildViolation('workflow.No dest users when the workflow is finalized')
->atPath('finalizeAfter')
->addViolation();
}
} else {
if (0 === \count($this->getDestUser())) {
$context->buildViolation('workflow.The next step must count at least one dest')
->atPath('finalizeAfter')
->addViolation();
}
public function addOnHold(EntityWorkflowStepHold $onHold): self
{
if (!$this->holdsOnStep->contains($onHold)) {
$this->holdsOnStep->add($onHold);
}
return $this;
}
}

View File

@@ -0,0 +1,54 @@
<?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\MainBundle\Entity\Workflow;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table('chill_main_workflow_entity_step_hold')]
#[ORM\UniqueConstraint(name: 'chill_main_workflow_hold_unique_idx', columns: ['step_id', 'byUser_id'])]
class EntityWorkflowStepHold implements TrackCreationInterface
{
use TrackCreationTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
public function __construct(#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class)]
#[ORM\JoinColumn(nullable: false)]
private EntityWorkflowStep $step, #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
private User $byUser)
{
$step->addOnHold($this);
}
public function getId(): ?int
{
return $this->id;
}
public function getStep(): EntityWorkflowStep
{
return $this->step;
}
public function getByUser(): User
{
return $this->byUser;
}
}

View File

@@ -0,0 +1,207 @@
<?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\MainBundle\Entity\Workflow;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'chill_main_workflow_entity_step_signature')]
class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
#[ORM\Id]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, unique: true)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: true)]
private ?User $userSigner = null;
#[ORM\ManyToOne(targetEntity: Person::class)]
#[ORM\JoinColumn(nullable: true)]
private ?Person $personSigner = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, nullable: false, enumType: EntityWorkflowSignatureStateEnum::class)]
private EntityWorkflowSignatureStateEnum $state = EntityWorkflowSignatureStateEnum::PENDING;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIMETZ_IMMUTABLE, nullable: true, options: ['default' => null])]
private ?\DateTimeImmutable $stateDate = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
private array $signatureMetadata = [];
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
private ?int $zoneSignatureIndex = null;
public function __construct(
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
#[ORM\JoinColumn(nullable: false)]
private EntityWorkflowStep $step,
User|Person $signer,
) {
$this->step->addSignature($this);
$this->setSigner($signer);
}
private function setSigner(User|Person $signer): void
{
if ($signer instanceof User) {
$this->userSigner = $signer;
} else {
$this->personSigner = $signer;
}
}
public function getId(): ?int
{
return $this->id;
}
public function getStep(): EntityWorkflowStep
{
return $this->step;
}
public function getSigner(): User|Person
{
if (null !== $this->userSigner) {
return $this->userSigner;
}
return $this->personSigner;
}
public function getSignatureMetadata(): array
{
return $this->signatureMetadata;
}
public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature
{
$this->signatureMetadata = $signatureMetadata;
return $this;
}
public function getState(): EntityWorkflowSignatureStateEnum
{
return $this->state;
}
/**
* @return $this
*
* @internal You should not use this method directly, use @see{Chill\MainBundle\Workflow\SignatureStepStateChanger} instead
*/
public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature
{
$this->state = $state;
return $this;
}
public function getStateDate(): ?\DateTimeImmutable
{
return $this->stateDate;
}
/**
* @return $this
*
* @internal You should not use this method directly, use @see{Chill\MainBundle\Workflow\SignatureStepStateChanger} instead
*/
public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature
{
$this->stateDate = $stateDate;
return $this;
}
public function getZoneSignatureIndex(): ?int
{
return $this->zoneSignatureIndex;
}
/**
* @return $this
*
* @internal You should not use this method directly, use @see{Chill\MainBundle\Workflow\SignatureStepStateChanger} instead
*/
public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature
{
$this->zoneSignatureIndex = $zoneSignatureIndex;
return $this;
}
public function isSigned(): bool
{
return EntityWorkflowSignatureStateEnum::SIGNED == $this->getState();
}
public function isPending(): bool
{
return EntityWorkflowSignatureStateEnum::PENDING == $this->getState();
}
public function isCanceled(): bool
{
return EntityWorkflowSignatureStateEnum::CANCELED === $this->getState();
}
public function isRejected(): bool
{
return EntityWorkflowSignatureStateEnum::REJECTED === $this->getState();
}
/**
* Checks whether all signatures associated with a given workflow step are not pending.
*
* Iterates over each signature in the provided workflow step, and returns false if any signature
* is found to be pending. If all signatures are not pending, returns true.
*
* @param EntityWorkflowStep $step the workflow step whose signatures are to be checked
*
* @return bool true if all signatures are not pending, false otherwise
*/
public static function isAllSignatureNotPendingForStep(EntityWorkflowStep $step): bool
{
foreach ($step->getSignatures() as $signature) {
if ($signature->isPending()) {
return false;
}
}
return true;
}
/**
* @return 'person'|'user'
*/
public function getSignerKind(): string
{
if ($this->personSigner instanceof Person) {
return 'person';
}
return 'user';
}
}

View File

@@ -0,0 +1,25 @@
<?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\MainBundle\Export;
/**
* Transform data from filter.
*
* This interface defines a method for transforming filter's form data before it is processed.
*
* You can implement this interface on @see{FilterInterface} or @see{AggregatorInterface}, to allow to transform existing data in saved exports
* and replace it with some default values, or new default values.
*/
interface DataTransformerInterface
{
public function transformData(?array $before): array;
}

View File

@@ -126,7 +126,7 @@ final readonly class ExportFormHelper
private function savedExportDataToFormDataStepExport(
SavedExport $savedExport,
array $formOptions
array $formOptions,
): array {
$builder = $this->formFactory
->createBuilder(
@@ -148,7 +148,7 @@ final readonly class ExportFormHelper
private function savedExportDataToFormDataStepFormatter(
SavedExport $savedExport,
array $formOptions
array $formOptions,
): array {
$builder = $this->formFactory
->createBuilder(

View File

@@ -62,7 +62,7 @@ class ExportManager
private readonly TokenStorageInterface $tokenStorage,
iterable $exports,
iterable $aggregators,
iterable $filters
iterable $filters,
// iterable $formatters,
// iterable $exportElementProvider
) {
@@ -190,7 +190,7 @@ class ExportManager
// throw an error if the export require other modifier, which is
// not allowed when the export return a `NativeQuery`
if (\count($export->supportsModifiers()) > 0) {
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\\Doctrine\\ORM\\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\\ORM\\QueryBuilder`');
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
}
} elseif ($query instanceof QueryBuilder) {
// handle filters
@@ -203,7 +203,7 @@ class ExportManager
'dql' => $query->getDQL(),
]);
} else {
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\\Doctrine\\ORM\\NativeQuery` or a `Doctrine\\ORM\\QueryBuilder` object.');
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
}
$result = $export->getResult($query, $data[ExportType::EXPORT_KEY]);
@@ -451,7 +451,7 @@ class ExportManager
public function isGrantedForElement(
DirectExportInterface|ExportInterface|ModifierInterface $element,
DirectExportInterface|ExportInterface|null $export = null,
?array $centers = null
?array $centers = null,
): bool {
if ($element instanceof ExportInterface || $element instanceof DirectExportInterface) {
$role = $element->requiredRole();
@@ -533,7 +533,7 @@ class ExportManager
ExportInterface $export,
QueryBuilder $qb,
array $data,
array $center
array $center,
) {
$aggregators = $this->retrieveUsedAggregators($data);
@@ -560,7 +560,7 @@ class ExportManager
ExportInterface $export,
QueryBuilder $qb,
mixed $data,
array $centers
array $centers,
) {
$filters = $this->retrieveUsedFilters($data);

View File

@@ -32,6 +32,9 @@ interface FilterInterface extends ModifierInterface
/**
* Get the default data, that can be use as "data" for the form.
*
* In case of adding new parameters to a filter, you can implement a @see{DataTransformerFilterInterface} to
* transforme the filters's data saved in an export to the desired state.
*/
public function getFormDefaultData(): array;

View File

@@ -51,7 +51,7 @@ class CSVFormatter implements FormatterInterface
public function __construct(
protected TranslatorInterface $translator,
ExportManager $manager
ExportManager $manager,
) {
$this->exportManager = $manager;
}
@@ -118,7 +118,7 @@ class CSVFormatter implements FormatterInterface
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData
array $aggregatorsData,
) {
$this->result = $result;
$this->orderingHeaders($formatterData);

View File

@@ -68,7 +68,7 @@ class CSVListFormatter implements FormatterInterface
public function buildForm(
FormBuilderInterface $builder,
$exportAlias,
array $aggregatorAliases
array $aggregatorAliases,
) {
$builder->add('numerotation', ChoiceType::class, [
'choices' => [
@@ -108,7 +108,7 @@ class CSVListFormatter implements FormatterInterface
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData
array $aggregatorsData,
) {
$this->result = $result;
$this->exportAlias = $exportAlias;

View File

@@ -66,7 +66,7 @@ class CSVPivotedListFormatter implements FormatterInterface
public function buildForm(
FormBuilderInterface $builder,
$exportAlias,
array $aggregatorAliases
array $aggregatorAliases,
) {
$builder->add('numerotation', ChoiceType::class, [
'choices' => [
@@ -107,7 +107,7 @@ class CSVPivotedListFormatter implements FormatterInterface
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData
array $aggregatorsData,
) {
$this->result = $result;
$this->exportAlias = $exportAlias;

View File

@@ -129,7 +129,7 @@ class SpreadSheetFormatter implements FormatterInterface
public function buildForm(
FormBuilderInterface $builder,
$exportAlias,
array $aggregatorAliases
array $aggregatorAliases,
) {
// choosing between formats
$builder->add('format', ChoiceType::class, [
@@ -178,7 +178,7 @@ class SpreadSheetFormatter implements FormatterInterface
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData
array $aggregatorsData,
): Response {
// store all data when the process is initiated
$this->result = $result;
@@ -219,7 +219,7 @@ class SpreadSheetFormatter implements FormatterInterface
protected function addContentTable(
Worksheet $worksheet,
$sortedResults,
$line
$line,
) {
$worksheet->fromArray(
$sortedResults,
@@ -274,7 +274,7 @@ class SpreadSheetFormatter implements FormatterInterface
protected function addHeaders(
Worksheet &$worksheet,
array $globalKeys,
$line
$line,
) {
// get the displayable form of headers
$displayables = [];

View File

@@ -72,7 +72,7 @@ class SpreadsheetListFormatter implements FormatterInterface
public function buildForm(
FormBuilderInterface $builder,
$exportAlias,
array $aggregatorAliases
array $aggregatorAliases,
) {
$builder
->add('format', ChoiceType::class, [
@@ -120,7 +120,7 @@ class SpreadsheetListFormatter implements FormatterInterface
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData
array $aggregatorsData,
) {
$this->result = $result;
$this->exportAlias = $exportAlias;

View File

@@ -31,7 +31,7 @@ interface FormatterInterface
public function buildForm(
FormBuilderInterface $builder,
$exportAlias,
array $aggregatorAliases
array $aggregatorAliases,
);
/**
@@ -58,7 +58,7 @@ interface FormatterInterface
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData
array $aggregatorsData,
);
public function getType();

View File

@@ -35,7 +35,7 @@ class IdToEntityDataTransformer implements DataTransformerInterface
public function __construct(
private readonly ObjectRepository $repository,
private readonly bool $multiple = false,
?callable $getId = null
?callable $getId = null,
) {
$this->getId = $getId ?? static fn (object $o) => $o->getId();
}

View File

@@ -0,0 +1,64 @@
<?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\MainBundle\Form;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Entity\GenderIconEnum;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class GenderType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('label', TranslatableStringFormType::class, [
'required' => true,
])
->add('icon', EnumType::class, [
'class' => GenderIconEnum::class,
'choices' => GenderIconEnum::cases(),
'expanded' => true,
'multiple' => false,
'mapped' => true,
'choice_label' => fn (GenderIconEnum $enum) => '<i class="'.strtolower($enum->value).'"></i>',
'choice_value' => fn (?GenderIconEnum $enum) => null !== $enum ? $enum->value : null,
'label' => 'gender.admin.Select gender icon',
'label_html' => true,
])
->add('genderTranslation', EnumType::class, [
'class' => GenderEnum::class,
'choice_label' => fn (GenderEnum $enum) => $enum->value,
'label' => 'gender.admin.Select gender translation',
])
->add('active', ChoiceType::class, [
'choices' => [
'Active' => true,
'Inactive' => false,
],
])
->add('order', NumberType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Gender::class,
]);
}
}

View File

@@ -29,6 +29,7 @@ class NewsItemType extends AbstractType
])
->add('content', ChillTextareaType::class, [
'required' => false,
'empty_data' => '',
])
->add(
'startDate',

View File

@@ -103,7 +103,7 @@ trait AppendScopeChoiceTypeTrait
AuthorizationHelper $authorizationHelper,
TranslatableStringHelper $translatableStringHelper,
ObjectManager $om,
$name = 'scope'
$name = 'scope',
) {
$reachableScopes = $authorizationHelper
->getReachableScopes($user, $role, $center);

View File

@@ -36,6 +36,7 @@ class ChillCollectionType extends AbstractType
$view->vars['identifier'] = $options['identifier'];
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
$view->vars['js_caller'] = $options['js_caller'];
$view->vars['uniqid'] = uniqid();
}
public function configureOptions(OptionsResolver $resolver)

View File

@@ -13,36 +13,30 @@ namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\PermissionsGroup;
use Doctrine\ORM\EntityRepository;
use Chill\MainBundle\Repository\CenterRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ComposedGroupCenterType extends AbstractType
{
public function __construct(private readonly CenterRepository $centerRepository) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$centers = $this->centerRepository->findActive();
$builder->add('permissionsgroup', EntityType::class, [
'class' => PermissionsGroup::class,
'choice_label' => static fn (PermissionsGroup $group) => $group->getName(),
])->add('center', EntityType::class, [
'class' => Center::class,
'query_builder' => static function (EntityRepository $er) {
$qb = $er->createQueryBuilder('c');
$qb->where($qb->expr()->eq('c.isActive', 'TRUE'))
->orderBy('c.name', 'ASC');
return $qb;
},
])->add('center', ChoiceType::class, [
'choices' => $centers,
'choice_label' => fn (Center $center) => $center->getName(),
'multiple' => true,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', \Chill\MainBundle\Entity\GroupCenter::class);
}
public function getBlockPrefix()
{
return 'composed_groupcenter';

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