Merge branch '323-related-entity-permission-give-from-workflow' into signature-app-master

This commit is contained in:
Julien Fastré 2024-11-13 22:44:29 +01:00
commit 903a87c589
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
86 changed files with 1444 additions and 446 deletions

View File

@ -1,6 +0,0 @@
kind: Fixed
body: Show only the current referrer in the page "show" for an accompanying period
workf
time: 2024-09-16T15:18:43.017401122+02:00
custom:
Issue: "308"

6
.changes/v3.1.1.md Normal file
View File

@ -0,0 +1,6 @@
## v3.1.1 - 2024-10-01
### Fixed
* ([#308](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/308)) Show only the current referrer in the page "show" for an accompanying period workf
* ([#309](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/309)) Correctly compute the grouping by referrer aggregator
* Fixed typing of custom field long choice and custom field group

3
.changes/v3.2.0.md Normal file
View File

@ -0,0 +1,3 @@
## v3.2.0 - 2024-10-30
### Feature
* Introduce a gender entity

4
.changes/v3.2.1.md Normal file
View File

@ -0,0 +1,4 @@
## v3.2.1 - 2024-10-31
### Fixed
* Add the possibility of unknown to the gender entity
* Fix the fusion of person doubles by excluding accompanyingPeriod work entities to be deleted. They are moved instead.

3
.changes/v3.2.2.md Normal file
View File

@ -0,0 +1,3 @@
## v3.2.2 - 2024-10-31
### Fixed
* Fix gender translation for unknown

4
.changes/v3.2.3.md Normal file
View File

@ -0,0 +1,4 @@
## v3.2.3 - 2024-11-05
### Fixed
* ([#315](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/315)) Fix display of accompanying period work referrers. Only current referrers should be displayed.
Fix color of Chill footer

3
.changes/v3.2.4.md Normal file
View File

@ -0,0 +1,3 @@
## v3.2.4 - 2024-11-06
### Fixed
* Fix compilation of chill assets

View File

@ -6,6 +6,35 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v3.2.4 - 2024-11-06
### Fixed
* Fix compilation of chill assets
## v3.2.3 - 2024-11-05
### Fixed
* ([#315](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/315)) Fix display of accompanying period work referrers. Only current referrers should be displayed.
Fix color of Chill footer
## v3.2.2 - 2024-10-31
### Fixed
* Fix gender translation for unknown
## v3.2.1 - 2024-10-31
### Fixed
* Add the possibility of unknown to the gender entity
* Fix the fusion of person doubles by excluding accompanyingPeriod work entities to be deleted. They are moved instead.
## v3.2.0 - 2024-10-30
### Feature
* Introduce a gender entity
## v3.1.1 - 2024-10-01
### Fixed
* ([#308](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/308)) Show only the current referrer in the page "show" for an accompanying period workf
* ([#309](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/309)) Correctly compute the grouping by referrer aggregator
* Fixed typing of custom field long choice and custom field group
## v3.1.0 - 2024-08-30
### Feature
* Add export aggregator to aggregate activities by household + filter persons that are not part of an accompanyingperiod during a certain timeframe.
@ -20,8 +49,14 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
### Feature
* ([#306](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/306)) When a document is converted or downloaded in the browser, this document is removed from the browser memory after 45s. Future click on the button re-download the document.
## v2.23.0 - 2024-07-19 & 2024-07-23
## v2.23.0 - 2024-07-23 & 2024-07-19
### Feature
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
* Add job bundle (module emploi)
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
* Upgrade CKEditor and refactor configuration with use of typescript
* ([#123](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/123)) Add a button to duplicate calendar ranges from a week to another one
* [admin] filter users by active / inactive in the admin user's list
* ([#273](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/273)) Add the possibility to mark all notifications as read
@ -31,6 +66,8 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
* Do not update the "createdAt" column when importing postal code which does not change
* Display filename on file upload within the UI interface
### Fixed
* Fix resolving of centers for an household, which will fix in turn the access control
* Resolved type hinting error in activity list export
* ([#271](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/271)) Take into account the acp closing date in the acp works date filter
### Traduction française des principaux changements
@ -43,16 +80,6 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
- Agrandit l'icône du type de fichier dans l'interface de dépôt de fichier;
- correction: tient compte de la date de fermeture du parcours dans les filtres sur les actions d'accompagnement.
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
* Add job bundle (module emploi)
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
* Upgrade CKEditor and refactor configuration with use of typescript
### Fixed
* Fix resolving of centers for an household, which will fix in turn the access control
* Resolved type hinting error in activity list export
## v2.22.2 - 2024-07-03
### Fixed
* Remove scope required for event participation stats

View File

@ -59,7 +59,8 @@
"vue-i18n": "^9.1.6",
"vue-multiselect": "3.0.0-alpha.2",
"vue-toast-notification": "^3.1.2",
"vuex": "^4.0.0"
"vuex": "^4.0.0",
"bootstrap-icons": "^1.11.3"
},
"browserslist": [
"Firefox ESR"

View File

@ -16,7 +16,7 @@ use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security;
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
@ -24,7 +24,7 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
public function __construct(
private readonly ActivityRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@ -42,8 +42,8 @@ class CustomFieldLongChoice extends AbstractCustomField
$translatableStringHelper = $this->translatableStringHelper;
$builder->add($customField->getSlug(), Select2ChoiceType::class, [
'choices' => $entries,
'choice_label' => static fn (Option $option) => $translatableStringHelper->localize($option->getText()),
'choice_value' => static fn (Option $key): ?int => null === $key ? null : $key->getId(),
'choice_label' => static fn (?Option $option) => $translatableStringHelper->localize($option->getText()),
'choice_value' => static fn (?Option $key): ?int => $key?->getId(),
'multiple' => false,
'expanded' => false,
'required' => $customField->isRequired(),

View File

@ -46,11 +46,8 @@ class CustomFieldsGroup
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
/**
* @var array
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
private $name;
private array|string $name;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
private array $options = [];
@ -181,7 +178,7 @@ class CustomFieldsGroup
*
* @return CustomFieldsGroup
*/
public function setName($name)
public function setName(array|string $name)
{
$this->name = $name;

View File

@ -15,7 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
@ -34,7 +34,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
public function __construct(
private readonly Security $security,
private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
) {}
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
@ -49,6 +49,11 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
// Retrieve the related accompanying course document
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
if ($this->workflowDocumentService->isAllowedByWorkflow($entity)) {
// read and write permissions are granted by workflow
return true;
}
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
$voterAttribute = $this->attributeToRole($attribute);

View File

@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security;
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
@ -24,7 +24,7 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb
public function __construct(
private readonly AccompanyingCourseDocumentRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security;
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
@ -24,7 +24,7 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
public function __construct(
private readonly PersonDocumentRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@ -15,7 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\MainBundle\Entity\User;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@ -30,16 +30,16 @@ class AbstractStoredObjectVoterTest extends TestCase
{
private AssociatedEntityToStoredObjectInterface $repository;
private Security $security;
private WorkflowStoredObjectPermissionHelper $workflowDocumentService;
private WorkflowRelatedEntityPermissionHelper $workflowDocumentService;
protected function setUp(): void
{
$this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class);
$this->security = $this->createMock(Security::class);
$this->workflowDocumentService = $this->createMock(WorkflowStoredObjectPermissionHelper::class);
$this->workflowDocumentService = $this->createMock(WorkflowRelatedEntityPermissionHelper::class);
}
private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null): AbstractStoredObjectVoter
private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null): AbstractStoredObjectVoter
{
// Anonymous class extending the abstract class
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
@ -47,7 +47,7 @@ class AbstractStoredObjectVoterTest extends TestCase
private readonly bool $canBeAssociatedWithWorkflow,
private readonly AssociatedEntityToStoredObjectInterface $repository,
Security $security,
?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
) {
parent::__construct($security, $workflowDocumentService);
}
@ -99,6 +99,25 @@ class AbstractStoredObjectVoterTest extends TestCase
$this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed);
}
public function testIsAllowedByWorkflow(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
$workflowRelatedEntityPermissionHelper = $this->createMock(WorkflowRelatedEntityPermissionHelper::class);
$workflowRelatedEntityPermissionHelper->method('isAllowedByWorkflow')->withAnyParameters()->willReturn(true);
$associatedObjectRepository = $this->createMock(AssociatedEntityToStoredObjectInterface::class);
$associatedObjectRepository->method('findAssociatedEntityToStoredObject')->willReturn($entity);
$voter = $this->buildStoredObjectVoter(
true,
$associatedObjectRepository,
$this->createMock(Security::class),
$workflowRelatedEntityPermissionHelper
);
self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::EDIT, $subject, $token));
}
public function testSupportsOnAttribute(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();

View File

@ -14,7 +14,7 @@ namespace Chill\EventBundle\Security\Authorization;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\EventVoter;
@ -25,7 +25,7 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter
public function __construct(
private readonly EventRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@ -1,19 +0,0 @@
footer.footer {
padding: 0;
background-color: white;
border-top: 1px solid grey;
div.sponsors {
p {
padding-bottom: 10px;
color: #000;
font-size: 16px;
}
background-color: white;
padding: 2em 0;
img {
display: block;
margin: auto;
}
}
}

View File

@ -1 +0,0 @@
require('./csconnectes.scss');

View File

@ -5,8 +5,7 @@ module.exports = function(encore, chillEntries)
personal_situation_edit_file = __dirname + '/Resources/public/module/personal_situation/index.js',
cv_edit_file = __dirname + '/Resources/public/module/cv_edit/index.js',
immersion_edit_file = __dirname + '/Resources/public/module/immersion_edit/index.js',
images = __dirname + '/Resources/public/images/index.js',
sass_styles = __dirname + '/Resources/public/sass/index.js'
images = __dirname + '/Resources/public/images/index.js'
;
encore.addEntry('dispositifs_edit', dispositif_edit_file);
@ -15,6 +14,4 @@ module.exports = function(encore, chillEntries)
encore.addEntry('images', images);
encore.addEntry('cs_cv', cv_edit_file);
chillEntries.push(sass_styles);
};

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

@ -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()
{
return 100;
}
public function load(ObjectManager $manager)
{
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

@ -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;
@ -54,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;
@ -66,6 +69,7 @@ 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;
@ -511,6 +515,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',
@ -814,6 +840,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

@ -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'])]
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
#[Serializer\Groups(['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'])]
#[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

@ -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

@ -0,0 +1,47 @@
<?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\Repository;
use Chill\MainBundle\Entity\Gender;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Gender>
*/
class GenderRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Gender::class);
}
public function findByActiveOrdered(): array
{
return $this->createQueryBuilder('g')
->select('g')
->where('g.active = True')
->orderBy('g.order', 'ASC')
->getQuery()
->getResult();
}
public function findByGenderTranslation($gender): array
{
return $this->createQueryBuilder('g')
->select('g')
->where('g.genderTranslation = :gender')
->setParameter('gender', $gender)
->getQuery()
->getResult();
}
}

View File

@ -10,6 +10,7 @@ import Modal from 'bootstrap/js/dist/modal';
import Collapse from 'bootstrap/js/src/collapse';
import Carousel from 'bootstrap/js/src/carousel';
import Popover from 'bootstrap/js/src/popover';
import 'bootstrap-icons/font/bootstrap-icons.css';
//
// Carousel: ACHeaderSlider is a small slider used in banner of AccompanyingCourse Section

View File

@ -0,0 +1,11 @@
<template>
<i :class="gender.icon"></i>
</template>
<script setup>
const props = defineProps({
gender: Object
})
</script>

View File

@ -38,7 +38,9 @@ const messages = {
person: "Usager",
birthday: {
man: "Né le",
woman: "Née le"
woman: "Née le",
neutral: "Né·e le",
unknown: "Né·e le",
},
deathdate: "Date de décès",
household_without_address: "Le ménage de l'usager est sans adresse",

View File

@ -0,0 +1,75 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %}
{% form_theme form _self %}
{% block _gender_icon_widget %}
{% for child in form %}
<div class="form-check">
<input
type="radio"
id="{{ child.vars.id }}"
name="{{ child.vars.full_name }}"
value="{{ child.vars.value }}"
{% if child.vars.checked %}checked="checked"{% endif %}
/>
<label for="{{ child.vars.id }}">{{ child.vars.label|raw }}</label>
</div>
{% endfor %}
{% endblock %}
{% block admin_content %}
{% set formId = crudMainFormId|default('crud_main_form') %}
{% block crud_content_header %}
<h1 class="mb-5">{{ ('crud.'~crud_name~'.title_edit')|trans }}</h1>
{% endblock crud_content_header %}
{% block crud_content_form %}
{{ form_start(form, { 'attr' : { 'id': formId } }) }}
{{ form_row(form.label) }}
{{ form_row(form.genderTranslation) }}
{{ form_row(form.icon) }}
{{ form_row(form.active) }}
{{ form_row(form.order) }}
{{ form_end(form) }}
{% block crud_content_after_form %}{% endblock %}
{% block crud_content_form_actions %}
<ul class="record_actions sticky-form-buttons">
{% block content_form_actions_back %}
<li class="cancel">
{# <a class="btn btn-cancel" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_index') }}">#}
{# {{ 'Cancel'|trans }}#}
{# </a>#}
</li>
{% endblock %}
{% block content_form_actions_before %}{% endblock %}
{% block content_form_actions_delete %}
{% if chill_crud_action_exists(crud_name, 'delete') %}
{% if is_granted(chill_crud_config('role', crud_name, 'delete'), entity) %}
<li class="">
<a class="btn btn-small btn-delete" href="{{ chill_path_add_return_path('chill_crud_'~crud_name~'_delete', { 'id': entity.id }) }}"></a>
</li>
{% endif %}
{% endif %}
{% endblock content_form_actions_delete %}
{% block content_form_actions_save_and_close %}
<li class="">
<button type="submit" name="submit" value="save-and-close" class="btn btn-update" form="{{ formId }}">
{{ 'crud.edit.save_and_close'|trans }}
</button>
</li>
{% endblock %}
{% block content_form_actions_after %}{% endblock %}
</ul>
{% endblock %}
{% endblock %}
{% endblock admin_content %}

View File

@ -0,0 +1,46 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% block table_entities_thead_tr %}
<th>id</th>
<th>{{ 'label'|trans }}</th>
<th>{{ 'icon'|trans }}</th>
<th>{{ 'gender.genderTranslation'|trans }}</th>
<th>{{ 'active'|trans }}</th>
<th>{{ 'ordering'|trans }}</th>
<th></th>
{% endblock %}
{% block table_entities_tbody %}
{% for entity in entities %}
<tr>
<td>{{ entity.id }}</td>
<td>{{ entity.label|localize_translatable_string }}</td>
<td>{{ entity.icon|chill_entity_render_box }}</td>
<td>{{ entity.genderTranslation.value }}</td>
<td style="text-align:center;">
{%- if entity.active -%}
<i class="fa fa-check-square-o"></i>
{%- else -%}
<i class="fa fa-square-o"></i>
{%- endif -%}
</td>
<td>{{ entity.order }}</td>
<td>
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_crud_main_gender_edit', { 'id': entity.id}) }}" class="btn btn-sm btn-edit btn-mini"></a>
</li>
</ul>
</td>
</tr>
{% endfor %}
{% endblock %}
{% block actions_before %}
<li class='cancel'>
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
</li>
{% endblock %}
{% endembed %}
{% endblock %}

View File

@ -0,0 +1,79 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %}
{% form_theme form _self %}
{% block _gender_icon_widget %}
{% for child in form %}
<div class="form-check">
<input
type="radio"
id="{{ child.vars.id }}"
name="{{ child.vars.full_name }}"
value="{{ child.vars.value }}"
{% if child.vars.checked %}checked="checked"{% endif %}
/>
<label for="{{ child.vars.id }}">{{ child.vars.label|raw }}</label>
</div>
{% endfor %}
{% endblock %}
{% block admin_content %}
{% set formId = crudMainFormId|default('crud_main_form') %}
{% block crud_content_header %}
<h1>{{ ('crud.' ~ crud_name ~ '.title_new')|trans({'%crud_name%' : crud_name }) }}</h1>
{% endblock crud_content_header %}
{% block crud_content_form %}
{{ form_start(form, { 'attr' : { 'id': formId } }) }}
{{ form_row(form.label) }}
{{ form_row(form.genderTranslation) }}
{{ form_row(form.icon) }}
{{ form_row(form.active) }}
{{ form_row(form.order) }}
{{ form_end(form) }}
{% block crud_content_after_form %}{% endblock %}
{% block crud_content_form_actions %}
<ul class="record_actions sticky-form-buttons">
{% block content_form_actions_back %}
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_index') }}">
{{ 'Cancel'|trans }}
</a>
</li>
{% endblock %}
{% block content_form_actions_save_and_close %}
<li class="">
<button type="submit" name="submit" value="save-and-close" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_close'|trans }}
</button>
</li>
{% endblock %}
{% block content_form_actions_save_and_show %}
<li class="">
<button type="submit" name="submit" value="save-and-show" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_show'|trans }}
</button>
</li>
{% endblock %}
{% block content_form_actions_save_and_new %}
<li class="">
<button type="submit" name="submit" value="save-and-new" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_new'|trans }}
</button>
</li>
{% endblock %}
</ul>
{% endblock %}
{{ form_end(form) }}
{% endblock %}
{% endblock admin_content %}

View File

@ -62,12 +62,12 @@ abstract class AbstractSearch implements SearchInterface
$recomposed .= ' '.$term.':';
$containsSpace = str_contains((string) $terms[$term], ' ');
if ($containsSpace) {
if ($containsSpace || is_numeric($terms[$term])) {
$recomposed .= '"';
}
$recomposed .= (false === mb_stristr(' ', (string) $terms[$term])) ? $terms[$term] : '('.$terms[$term].')';
if ($containsSpace) {
if ($containsSpace || is_numeric($terms[$term])) {
$recomposed .= '"';
}
}

View File

@ -35,7 +35,7 @@ interface ChillEntityRenderInterface
*
* @phpstan-pure
*/
public function renderBox($entity, array $options): string;
public function renderBox(mixed $entity, array $options): string;
/**
* Return the entity as a string.
@ -46,7 +46,7 @@ interface ChillEntityRenderInterface
*
* @phpstan-pure
*/
public function renderString($entity, array $options): string;
public function renderString(mixed $entity, array $options): string;
/**
* Return true if the class support this object for the given options.

View File

@ -0,0 +1,35 @@
<?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\Templating\Entity;
use Chill\MainBundle\Entity\GenderIconEnum;
/**
* @implements ChillEntityRenderInterface<GenderIconEnum>
*/
final readonly class ChillGenderIconRender implements ChillEntityRenderInterface
{
public function renderBox($icon, array $options): string
{
return '<i class="'.htmlspecialchars($icon->value, ENT_QUOTES, 'UTF-8').'"></i>';
}
public function renderString($icon, array $options): string
{
return $icon->value;
}
public function supports($icon, array $options): bool
{
return $icon instanceof GenderIconEnum;
}
}

View File

@ -9,9 +9,9 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Tests\Service;
namespace Chill\MainBundle\Tests\Workflow\Helper;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
@ -36,7 +36,7 @@ use Symfony\Component\Workflow\WorkflowInterface;
*
* @coversNothing
*/
class WorkflowStoredObjectPermissionHelperTest extends TestCase
class WorkflowRelatedEntityPermissionHelperTest extends TestCase
{
use ProphecyTrait;
@ -53,6 +53,19 @@ class WorkflowStoredObjectPermissionHelperTest extends TestCase
self::assertEquals($expected, $helper->notBlockedByWorkflow($entityWorkflow), $message);
}
/**
* @dataProvider provideDataAllowedByWorkflow
*/
public function testAllowedByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void
{
// all entities must have this workflow name, so we are ok to set it here
$entityWorkflow->setWorkflowName('dummy');
$object = new \stdClass();
$helper = $this->buildHelper($object, $entityWorkflow, $user);
self::assertEquals($expected, $helper->isAllowedByWorkflow($entityWorkflow), $message);
}
public function testNoWorkflow(): void
{
$object = new \stdClass();
@ -60,7 +73,7 @@ class WorkflowStoredObjectPermissionHelperTest extends TestCase
self::assertTrue($helper->notBlockedByWorkflow($object), "the user is not blocked by the user, as there aren't any user inside");
}
private function buildHelper(object $relatedEntity, ?EntityWorkflow $entityWorkflow, User $user): WorkflowStoredObjectPermissionHelper
private function buildHelper(object $relatedEntity, ?EntityWorkflow $entityWorkflow, User $user): WorkflowRelatedEntityPermissionHelper
{
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user);
@ -72,7 +85,55 @@ class WorkflowStoredObjectPermissionHelperTest extends TestCase
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([]);
}
return new WorkflowStoredObjectPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry());
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry());
}
public static function provideDataAllowedByWorkflow(): iterable
{
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [$entityWorkflow, new User(), false, 'not allowed because the user is not present as a dest user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [$entityWorkflow, $user, true, 'allowed because the user is a current user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [$entityWorkflow, $user, true, 'allowed because the user was a previous user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), new User());
$entityWorkflow->getCurrentStep()->setIsFinal(true);
yield [$entityWorkflow, $user, false, 'not allowed because: user was a previous user, but it is finalized positive'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable());
$entityWorkflow->getCurrentStep()->setIsFinal(true);
yield [$entityWorkflow, $user, true, 'allowed: user was a previous user, it is finalized, but finalized negative'];
}
public static function provideDataNotBlockByWorkflow(): iterable

View File

@ -9,7 +9,7 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Service;
namespace Chill\MainBundle\Workflow\Helper;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
@ -19,7 +19,7 @@ use Symfony\Component\Workflow\Registry;
/**
* Check if an object, associated with a workflow, is blocked, or not, by this workflow.
*/
class WorkflowStoredObjectPermissionHelper
class WorkflowRelatedEntityPermissionHelper
{
public function __construct(
private readonly Security $security,
@ -27,6 +27,38 @@ class WorkflowStoredObjectPermissionHelper
private readonly Registry $registry,
) {}
public function isAllowedByWorkflow(object $entity): bool
{
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
$currentUser = $this->security->getUser();
foreach ($entityWorkflows as $entityWorkflow) {
// if the user is finalized, we have to check if the workflow is finalPositive, or not
if ($entityWorkflow->isFinal()) {
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
foreach ($marking->getPlaces() as $place => $int) {
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
if (true === ($placeMetadata['isFinalPositive'] ?? false)) {
// the workflow is final, and final positive, so we stop here.
return false;
}
}
}
}
foreach ($entityWorkflows as $entityWorkflow) {
// so, the workflow is running... We return true if the current user is involved
foreach ($entityWorkflow->getSteps() as $step) {
if ($step->getAllDestUser()->contains($currentUser)) {
return true;
}
}
}
return false;
}
/**
* Return true if the user is allowed to update the given object.
*

View File

@ -54,6 +54,8 @@ services:
Chill\MainBundle\Templating\Entity\NewsItemRender: ~
Chill\MainBundle\Templating\Entity\ChillGenderIconRender: ~
Chill\MainBundle\Templating\Entity\UserRender: ~
Chill\MainBundle\Templating\Entity\UserGroupRender: ~

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20240926093955 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create gender table and default entities';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_main_gender_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_main_gender (id INT NOT NULL, label JSON NOT NULL, active BOOLEAN NOT NULL, genderTranslation VARCHAR(255) NOT NULL, icon VARCHAR(255) NOT NULL, ordering DOUBLE PRECISION DEFAULT \'0.0\', PRIMARY KEY(id))');
// Insert the four gender records into the chill_main_gender table
$this->addSql("
INSERT INTO chill_main_gender (id, label, active, genderTranslation, icon, ordering)
VALUES
(nextval('chill_main_gender_id_seq'),
'{\"fr\": \"homme\", \"nl\": \"man\"}',
true,
'man',
'bi bi-gender-male',
1.0
),
(nextval('chill_main_gender_id_seq'),
'{\"fr\": \"femme\", \"nl\": \"vrouw\"}',
true,
'woman',
'bi bi-gender-female',
1.1
),
(nextval('chill_main_gender_id_seq'),
'{\"fr\": \"neutre\", \"nl\": \"neutraal\"}',
true,
'neutral',
'bi bi-gender-neuter',
1.1
),
(nextval('chill_main_gender_id_seq'),
'{\"fr\": \"inconnu\", \"nl\": \"ongekend\"}',
true,
'unknown',
'bi bi-question',
1.2
)
");
}
public function down(Schema $schema): void
{
$this->addSql('DROP SEQUENCE chill_main_gender_id_seq CASCADE');
$this->addSql('DROP TABLE chill_main_gender');
}
}

View File

@ -43,6 +43,7 @@ lifecycleUpdate: Evenements de création et mise à jour
address_fields: Données liées à l'adresse
Datas: Données
No title: Aucun titre
icon: icône
user:
profile:
@ -474,6 +475,12 @@ crud:
title_delete: Supprimer une actualité
button_delete: Supprimer
confirm_message_delete: Êtes-vous sûr de vouloir supprimer l'actualité, "%as_string%" ?
main_gender:
index:
title: Liste des genres
add_new: Ajouter un genre
title_new: Nouveau genre
title_edit: Modifier un genre
No entities: Aucun élément
@ -781,4 +788,9 @@ news:
read_more: Lire la suite
show_details: Voir l'actualité
gender:
genderTranslation: traduction grammaticale
not defined: Non défini
pick gender: Choisir une genre

View File

@ -201,7 +201,7 @@ class PersonMove
private function getDeleteEntities(): array
{
return [
AccompanyingPeriod\AccompanyingPeriodWork::class,
// AccompanyingPeriod\AccompanyingPeriodWork::class,
Relationship::class,
];
}
@ -216,7 +216,7 @@ class PersonMove
}
/**
* get the full table name with schema if it does exists.
* get the full table name with schema if it exists.
*/
private function getTableName(ClassMetadata $metadata): string
{

View File

@ -15,11 +15,14 @@ use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CenterRepository;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Repository\GenderRepository;
use Chill\MainBundle\Repository\ScopeRepository;
use Chill\MainBundle\Repository\UserRepository;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
@ -77,12 +80,15 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
*/
protected array $cacheUsers = [];
/**
* @var array|Gender[]
*/
protected array $cacheGenders = [];
protected Generator $faker;
protected NativeLoader $loader;
private array $genders = [Person::MALE_GENDER, Person::FEMALE_GENDER, Person::BOTH_GENDER];
private array $peoples = [
[
'lastName' => 'Depardieu',
@ -90,7 +96,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
'birthdate' => '1948-12-27',
'placeOfBirth' => 'Châteauroux',
'nationality' => 'RU',
'gender' => Person::MALE_GENDER,
'gender' => GenderEnum::MALE,
'center' => 'Center A',
'accompanyingPeriods' => [
[
@ -119,7 +125,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
'maritalStatus' => 'ms_divorce',
],
[
// to have a person with same birthdate of Gérard Depardieu
// to have a person with same birthdate as Gérard Depardieu
'lastName' => 'Van Snick',
'firstName' => 'Bart',
'birthdate' => '1948-12-27',
@ -131,7 +137,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
'lastName' => 'Depardieu',
'firstName' => 'Charline',
'birthdate' => '1970-10-15',
'gender' => Person::FEMALE_GENDER,
'gender' => GenderEnum::FEMALE,
'center' => 'Center A',
'maritalStatus' => 'ms_legalco',
],
@ -228,6 +234,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
protected MaritalStatusRepository $maritalStatusRepository,
protected ScopeRepository $scopeRepository,
protected UserRepository $userRepository,
protected GenderRepository $genderRepository,
) {
$this->faker = Factory::create('fr_FR');
$this->faker->addProvider($this);
@ -272,9 +279,17 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
/**
* @internal This method is public and called by faker as a custom generator
*/
public function getRandomGender(): string
public function getRandomGender(int $nullPercentage = 50): ?Gender
{
return $this->genders[array_rand($this->genders)];
if (0 === \count($this->cacheGenders)) {
$this->cacheGenders = $this->genderRepository->findByActiveOrdered();
}
if (\random_int(0, 100) > $nullPercentage) {
return null;
}
return $this->cacheGenders[array_rand($this->cacheGenders)];
}
/**

View File

@ -21,6 +21,7 @@ use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Entity\User;
@ -62,19 +63,11 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[HouseholdMembershipSequential(groups: ['household_memberships'])]
class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateInterface, \Stringable
{
final public const BOTH_GENDER = 'both';
// have days in commun
final public const ERROR_ADDIND_PERIOD_AFTER_AN_OPEN_PERIOD = 2; // where there exist
final public const ERROR_PERIODS_ARE_COLLAPSING = 1; // when two different periods
final public const FEMALE_GENDER = 'woman';
final public const MALE_GENDER = 'man';
final public const NO_INFORMATION = 'unknown';
/**
* Accept receiving email.
*/
@ -245,11 +238,11 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
private ?string $fullnameCanonical = '';
/**
* The person's gender.
* NEW column : The person's gender.
*/
#[Assert\NotNull(message: 'The gender must be set')]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 9, nullable: true)]
private ?string $gender = null;
#[ORM\ManyToOne(targetEntity: Gender::class)]
private ?Gender $gender = null;
/**
* Comment on gender.
@ -1041,7 +1034,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this->fullnameCanonical;
}
public function getGender(): ?string
public function getGender(): ?Gender
{
return $this->gender;
}
@ -1051,24 +1044,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this->genderComment;
}
/**
* return gender as a Numeric form.
* This is used for translations.
*
* @return int
*
* @deprecated Keep for legacy. Used in Chill 1.5 for feminize before icu translations
*/
public function getGenderNumeric()
{
return match ($this->getGender()) {
self::FEMALE_GENDER => 1,
self::MALE_GENDER => 0,
self::BOTH_GENDER => 2,
default => -1,
};
}
public function getHouseholdAddresses(): Collection
{
return $this->householdAddresses;
@ -1570,7 +1545,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this;
}
public function setGender(?string $gender): self
public function setGender(?Gender $gender): self
{
$this->gender = $gender;

View File

@ -55,7 +55,7 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface, DataTrans
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_userHistory.endDate"),
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
$qb->expr()->lt('COALESCE(acp.openingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
)
),
$qb->expr()->andX(

View File

@ -12,7 +12,8 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Aggregator\PersonAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Repository\GenderRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
@ -20,7 +21,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class GenderAggregator implements AggregatorInterface
{
public function __construct(private TranslatorInterface $translator) {}
public function __construct(private TranslatorInterface $translator, private TranslatableStringHelperInterface $translatableStringHelper, private GenderRepository $repository) {}
public function addRole(): ?string
{
@ -29,7 +30,8 @@ final readonly class GenderAggregator implements AggregatorInterface
public function alterQuery(QueryBuilder $qb, $data)
{
$qb->addSelect('person.gender as gender');
$qb->leftJoin('person.gender', 'g');
$qb->addSelect('g.id as gender');
$qb->addGroupBy('gender');
}
@ -48,30 +50,20 @@ final readonly class GenderAggregator implements AggregatorInterface
public function getLabels($key, array $values, $data)
{
return function ($value) {
switch ($value) {
case Person::FEMALE_GENDER:
return $this->translator->trans('woman');
case Person::MALE_GENDER:
return $this->translator->trans('man');
case Person::BOTH_GENDER:
return $this->translator->trans('both');
case Person::NO_INFORMATION:
return $this->translator->trans('unknown');
case null:
case '':
return $this->translator->trans('Not given');
case '_header':
return $this->translator->trans('Gender');
default:
throw new \LogicException(sprintf('The value %s is not valid', $value));
return function (int|string|null $value) {
if (null === $value || '' === $value) {
return '';
}
if ('_header' === $value) {
return $this->translator->trans('Gender');
}
if (null === $gender = $this->repository->find((int) $value)) {
return '';
}
return (string) $this->translatableStringHelper->localize($gender->getLabel());
};
}

View File

@ -11,11 +11,13 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Filter\PersonFilters;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Repository\GenderRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
@ -24,17 +26,15 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class GenderFilter implements
ExportElementValidatedInterface,
FilterInterface
FilterInterface,
DataTransformerInterface
{
/**
* @var TranslatorInterface
*/
protected $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
// inject gender repository and find the active genders so that you can pass them to the ChoiceType (ordered by ordering)
public function __construct(
private readonly TranslatorInterface $translator,
private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly GenderRepository $genderRepository,
) {}
public function addRole(): ?string
{
@ -46,23 +46,17 @@ class GenderFilter implements
$where = $qb->getDQLPart('where');
$isIn = $qb->expr()->in('person.gender', ':person_gender');
if (!\in_array('null', $data['accepted_genders'], true)) {
$acceptedGenders = $data['accepted_genders_entity'];
$nullIncluded = in_array(null, $acceptedGenders ?? [], true);
if (!$nullIncluded) {
$clause = $isIn;
} else {
$clause = $qb->expr()->orX($isIn, $qb->expr()->isNull('person.gender'));
}
if ($where instanceof Expr\Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->add('where', $where);
$qb->setParameter('person_gender', \array_filter(
$data['accepted_genders'],
static fn ($el) => 'null' !== $el
));
$qb->andWhere($clause);
$qb->setParameter('person_gender', array_filter($acceptedGenders ?? [], fn ($gender) => null !== $gender));
}
public function applyOn()
@ -72,19 +66,42 @@ class GenderFilter implements
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('accepted_genders', ChoiceType::class, [
'choices' => [
'Woman' => Person::FEMALE_GENDER,
'Man' => Person::MALE_GENDER,
'Both' => Person::BOTH_GENDER,
'Unknown' => Person::NO_INFORMATION,
'Not given' => 'null',
],
$genderChoices = $this->genderRepository->findByActiveOrdered();
$choices = ['None' => null];
foreach ($genderChoices as $gender) {
$choices[$this->translatableStringHelper->localize($gender->getLabel())] = $gender->getId();
}
$builder->add('accepted_genders_entity', ChoiceType::class, [
'choices' => $choices,
'multiple' => true,
'expanded' => true,
'placeholder' => 'Select gender',
]);
}
public function transformData(?array $before): array
{
$transformedData = [];
$transformedData['accepted_genders_entity'] = $before['accepted_genders_entity'] ?? [];
if (array_key_exists('accepted_genders', $before)) {
foreach ($before['accepted_genders'] as $genderBefore) {
foreach ($this->genderRepository->findByGenderTranslation(
match ($genderBefore) {
'both' => 'neutral',
default => $genderBefore,
}
) as $gender) {
$transformedData['accepted_genders_entity'][] = $gender;
}
}
}
return $transformedData;
}
public function getFormDefaultData(): array
{
return [];
@ -94,11 +111,11 @@ class GenderFilter implements
{
$genders = [];
foreach ($data['accepted_genders'] as $g) {
if ('null' === $g) {
$genders[] = $this->translator->trans('Not given');
foreach ($data['accepted_genders_entity'] as $g) {
if (null === $g) {
$genders[] = $this->translator->trans('export.filter.person.gender.no_gender');
} else {
$genders[] = $this->translator->trans($g);
$genders[] = $this->translatableStringHelper->localize($this->genderRepository->find($g)->getLabel());
}
}
@ -120,7 +137,7 @@ class GenderFilter implements
public function validateForm($data, ExecutionContextInterface $context)
{
if (!\is_array($data['accepted_genders']) || 0 === \count($data['accepted_genders'])) {
if (!\is_iterable($data['accepted_genders_entity']) || 0 === \count($data['accepted_genders_entity'])) {
$context->buildViolation('You should select an option')
->addViolation();
}

View File

@ -16,6 +16,7 @@ use Chill\MainBundle\Export\Helper\ExportAddressHelper;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\CivilityRepositoryInterface;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Repository\GenderRepository;
use Chill\MainBundle\Repository\LanguageRepositoryInterface;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
@ -80,6 +81,7 @@ final readonly class ListPersonHelper
private TranslatableStringHelper $translatableStringHelper,
private TranslatorInterface $translator,
private UserRepositoryInterface $userRepository,
private GenderRepository $genderRepository,
/**
* @var iterable<CustomizeListPersonHelperInterface>
*/
@ -173,6 +175,11 @@ final readonly class ListPersonHelper
break;
case 'gender':
$qb->addSelect('IDENTITY(person.gender) AS gender');
break;
case 'maritalStatus':
$qb->addSelect('IDENTITY(person.maritalStatus) AS maritalStatus');
@ -325,7 +332,13 @@ final readonly class ListPersonHelper
return $this->translator->trans($key);
}
return $this->translator->trans($value);
if (null === $value) {
return '';
}
$gender = $this->genderRepository->find($value);
return $this->translatableStringHelper->localize($gender->getLabel());
};
case 'maritalStatus':

View File

@ -20,8 +20,8 @@ use Chill\MainBundle\Form\Type\PickCenterType;
use Chill\MainBundle\Form\Type\PickCivilityType;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\Type\GenderType;
use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\PersonBundle\Form\Type\PickGenderType;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use libphonenumber\PhoneNumberType;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
@ -60,7 +60,7 @@ final class CreationPersonType extends AbstractType
'label' => 'Civility',
'placeholder' => 'choose civility',
])
->add('gender', GenderType::class, [
->add('gender', PickGenderType::class, [
'required' => true, 'placeholder' => null,
])
->add('birthdate', ChillDateType::class, [

View File

@ -24,9 +24,9 @@ use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonPhone;
use Chill\PersonBundle\Form\Type\GenderType;
use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\PersonBundle\Form\Type\PersonPhoneType;
use Chill\PersonBundle\Form\Type\PickGenderType;
use Chill\PersonBundle\Form\Type\Select2MaritalStatusType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
@ -80,7 +80,7 @@ class PersonType extends AbstractType
'input' => 'datetime_immutable',
'widget' => 'single_text',
])
->add('gender', GenderType::class, [
->add('gender', PickGenderType::class, [
'required' => true,
])
->add('genderComment', CommentType::class, [

View File

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Form\Type;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* A type to select the civil union state.
*/
class GenderType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$a = [
Person::MALE_GENDER => Person::MALE_GENDER,
Person::FEMALE_GENDER => Person::FEMALE_GENDER,
Person::BOTH_GENDER => Person::BOTH_GENDER,
];
$resolver->setDefaults([
'choices' => $a,
'expanded' => true,
'multiple' => false,
'placeholder' => null,
]);
}
public function getParent()
{
return ChoiceType::class;
}
}

View File

@ -0,0 +1,51 @@
<?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\PersonBundle\Form\Type;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* A type to select the civil union state.
*/
class PickGenderType extends AbstractType
{
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper) {}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('label', 'Gender')
->setDefault(
'choice_label',
fn (Gender $gender): string => $this->translatableStringHelper->localize($gender->getLabel())
)
->setDefault(
'query_builder',
static fn (EntityRepository $er): QueryBuilder => $er->createQueryBuilder('g')
->where('g.active = true')
->orderBy('g.order'),
)
->setDefault('placeholder', 'choose gender')
->setDefault('class', Gender::class);
}
public function getParent()
{
return EntityType::class;
}
}

View File

@ -45,13 +45,17 @@ class AdminPersonMenuBuilder implements LocalMenuBuilderInterface
'route' => 'chill_crud_main_civility_index',
])->setExtras(['order' => 2010]);
$menu->addChild('Gender', [
'route' => 'chill_crud_main_gender_index',
])->setExtras(['order' => 2020]);
$menu->addChild('Marital status', [
'route' => 'chill_crud_person_marital-status_index',
])->setExtras(['order' => 2020]);
])->setExtras(['order' => 2030]);
$menu->addChild('person_admin.person_resource_kind', [
'route' => 'chill_crud_person_resource-kind_index',
])->setExtras(['order' => 2030]);
])->setExtras(['order' => 2040]);
}
public static function getMenuIds(): array

View File

@ -34,7 +34,7 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
?string $gender = null,
?int $gender = null,
?string $countryCode = null,
?string $phonenumber = null,
?string $city = null,
@ -62,7 +62,7 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
?string $gender = null,
?int $gender = null,
?string $countryCode = null,
?string $phonenumber = null,
?string $city = null,
@ -96,7 +96,7 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
?string $gender = null,
?int $gender = null,
?string $countryCode = null,
?string $phonenumber = null,
?string $city = null,
@ -202,7 +202,7 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
}
if (null !== $gender) {
$query->andWhereClause('person.gender = ?', [$gender]);
$query->andWhereClause('person.gender_id = ?', [$gender]);
}
return $query;
@ -253,7 +253,7 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
?string $gender = null,
?int $gender = null,
?string $countryCode = null,
?string $phonenumber = null,
?string $city = null,

View File

@ -23,7 +23,7 @@ interface PersonACLAwareRepositoryInterface
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
?string $gender = null,
?int $gender = null,
?string $countryCode = null,
?string $phonenumber = null,
?string $city = null,
@ -36,7 +36,7 @@ interface PersonACLAwareRepositoryInterface
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
?string $gender = null,
?int $gender = null,
?string $countryCode = null,
?string $phonenumber = null,
?string $city = null,
@ -55,7 +55,7 @@ interface PersonACLAwareRepositoryInterface
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
?string $gender = null,
?int $gender = null,
?string $countryCode = null,
?string $phonenumber = null,
?string $city = null,

View File

@ -128,7 +128,7 @@ export default {
body.mobilenumber = payload.data.mobilenumber;
body.email = payload.data.email;
body.altNames = payload.data.altNames;
body.gender = payload.data.gender;
body.gender = {id: payload.data.gender.id, type: payload.data.gender.type };
if (payload.data.civility !== null) { body.civility = {id: payload.data.civility.id, type: payload.data.civility.type }; }
makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body)

View File

@ -153,7 +153,7 @@ export default {
body.mobilenumber = payload.data.mobilenumber;
body.email = payload.data.email;
body.altNames = payload.data.altNames;
body.gender = payload.data.gender;
body.gender = {id: payload.data.gender.id, type: payload.data.gender.type };
if (payload.data.civility !== null) { body.civility = {id: payload.data.civility.id, type: payload.data.civility.type}; }
makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body)

View File

@ -8,7 +8,7 @@
{{ $t('household_members_editor.holder') }}
</span>
</div>
<div v-if="conc.person.birthdate !== null">{{ $t('person.born', {'gender': conc.person.gender} ) }}</div>
<div v-if="conc.person.birthdate !== null">{{ $t('person.born', {'gender': conc.person.gender.genderTranslation} ) }}</div>
</div>
<div class="item-col">
<ul class="list-content fa-ul">

View File

@ -12,10 +12,6 @@ const visMessages = {
Holder: 'Titulaire',
Legend: 'Calques',
concerned: 'concerné',
// both: 'neutre, non binaire',
woman: 'féminin',
man: 'masculin',
undefined: "genre non précisé",
years: 'ans',
click_to_expand: 'cliquez pour étendre',
add_relationship_link: "Créer un lien de filiation",
@ -64,7 +60,7 @@ const visMessages = {
placeholder: "Choisissez le genre de l'usager",
woman: "Féminin",
man: "Masculin",
both: "Neutre, non binaire",
neutral: "Neutre, non binaire",
undefined: "Non renseigné",
unknown: "Non renseigné"
}

View File

@ -1,6 +1,6 @@
import { createStore } from 'vuex'
import { getHouseholdByPerson, getCoursesByPerson, getRelationshipsByPerson } from './api'
import { getHouseholdLabel, getHouseholdWidth, getRelationshipLabel, getRelationshipTitle, getRelationshipDirection, splitId, getGender, getAge } from './vis-network'
import { getHouseholdLabel, getHouseholdWidth, getRelationshipLabel, getRelationshipTitle, getRelationshipDirection, splitId, getAge } from './vis-network'
import {visMessages} from "./i18n";
import { darkBlue, darkBrown, darkGreen, lightBlue, lightBrown, lightGreen } from './colors';
@ -148,7 +148,7 @@ const store = createStore({
person.group = person.type
person._id = person.id
person.id = `person_${person.id}`
person.label = `*${person.text}${person.deathdate ? ' (‡)' : ''}*\n_${getGender(person.gender)}${age}_${debug}`
person.label = `*${person.text}${person.deathdate ? ' (‡)' : ''}*\n_${person.gender.label.fr}${age}_${debug}`
person.folded = false
// folded is used for missing persons
if (options.folded) {

View File

@ -141,25 +141,6 @@ window.options = {
}
}
/**
* @param gender
* @returns {string}
*/
const getGender = (gender) => {
switch (gender) {
case 'both':
return visMessages.fr.visgraph.both
case 'woman':
return visMessages.fr.visgraph.woman
case 'man':
return visMessages.fr.visgraph.man
case 'unknown':
return visMessages.fr.visgraph.unknown
default:
return visMessages.fr.visgraph.undefined
}
}
/**
* TODO only one abstract function (-> getAge() is repeated in PersonRenderBox.vue)
* @param person
@ -251,7 +232,6 @@ const splitId = (id, position) => {
}
export {
getGender,
getAge,
getHouseholdLabel,
getHouseholdWidth,

View File

@ -24,6 +24,13 @@ const getCivilities = () =>
throw Error('Error with request resource response');
});
const getGenders = () => makeFetch("GET", '/api/1.0/main/gender.json')
// .then(response => {
// console.log(response)
// if (response.ok) { return response.json(); }
// throw Error('Error with request resource response');
// });
const getCentersForPersonCreation = () => makeFetch('GET', '/api/1.0/person/creation/authorized-centers', null);
/*
@ -63,10 +70,11 @@ const patchPerson = (id, body) => {
};
export {
getCentersForPersonCreation,
getCentersForPersonCreation,
getPerson,
getPersonAltNames,
getCivilities,
getGenders,
postPerson,
patchPerson
};

View File

@ -37,10 +37,9 @@
</div>
<p v-if="options.addInfo === true" class="moreinfo">
<i :class="'fa fa-fw ' + getGenderIcon" :title="$t(getGender)"></i>
<gender-icon-render-box v-if="person.gender" :gender="person.gender"></gender-icon-render-box>
<time v-if="person.birthdate && !person.deathdate" :datetime="person.birthdate" :title="birthdate">
{{ $t(getGenderTranslation) + ' ' + $d(birthdate, 'text') }}
{{ $t(person.gender ? `renderbox.birthday.${person.gender.genderTranslation}` : 'renderbox.birthday.neutral') + ' ' + $d(birthdate, 'text') }}
</time>
<time v-else-if="person.birthdate && person.deathdate" :datetime="person.deathdate"
@ -180,6 +179,7 @@
<script>
import {dateToISO, ISOToDate} from 'ChillMainAssets/chill/js/date';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import GenderIconRenderBox from 'ChillMainAssets/vuejs/_components/Entity/GenderIconRenderBox.vue'
import Confidential from 'ChillMainAssets/vuejs/_components/Confidential.vue';
import BadgeEntity from 'ChillMainAssets/vuejs/_components/BadgeEntity.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
@ -190,6 +190,7 @@ export default {
name: "PersonRenderBox",
components: {
AddressRenderBox,
GenderIconRenderBox,
Confidential,
BadgeEntity,
PersonText,
@ -222,15 +223,6 @@ export default {
return false
}
},
getGenderIcon: function () {
return this.person.gender === 'woman' ? 'fa-venus' : this.person.gender === 'man' ? 'fa-mars' : this.person.gender === 'both' ? 'fa-neuter' : 'fa-genderless';
},
getGenderTranslation: function () {
return this.person.gender === 'woman' ? 'renderbox.birthday.woman' : 'renderbox.birthday.man';
},
getGender() {
return this.person.gender === 'woman' ? 'person.gender.woman' : this.person.gender === 'man' ? 'person.gender.man' : this.person.gender === 'both' ? 'person.gender.both' : 'person.gender.undefined';
},
birthdate: function () {
if (this.person.birthdate !== null || this.person.birthdate === "undefined") {
return ISOToDate(this.person.birthdate.datetime);

View File

@ -73,17 +73,16 @@
<!-- TODO fix placeholder if undefined
-->
<div class="form-floating mb-3">
<select
<select
class="form-select form-select-lg"
id="gender"
v-model="gender"
@change="checkErrors"
>
<option selected disabled >{{ $t('person.gender.placeholder') }}</option>
<option value="woman">{{ $t('person.gender.woman') }}</option>
<option value="man">{{ $t('person.gender.man') }}</option>
<option value="both">{{ $t('person.gender.both') }}</option>
</select>
>
<option selected disabled >{{ $t('person.gender.placeholder') }}</option>
<option v-for="g in config.genders" :value="g.id" :key="g.id">
{{ g.label.fr }}
</option>
</select>
<label>{{ $t('person.gender.title') }}</label>
</div>
@ -178,7 +177,7 @@
</template>
<script>
import { getCentersForPersonCreation, getCivilities, getPerson, getPersonAltNames } from '../../_api/OnTheFly';
import { getCentersForPersonCreation, getCivilities, getGenders, getPerson, getPersonAltNames } from '../../_api/OnTheFly';
import PersonRenderBox from '../Entity/PersonRenderBox.vue';
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
@ -204,6 +203,7 @@ export default {
altNames: [],
civilities: [],
centers: [],
genders: []
},
showCenters: false, // NOTE: must remains false if the form is not in create mode
showAddressFormValue: false,
@ -237,8 +237,8 @@ export default {
get() { return this.person.lastName; }
},
gender: {
set(value) { this.person.gender = value; },
get() { return this.person.gender; }
set(value) { this.person.gender = {id: value, type: 'chill_main_gender'}; },
get() { return this.person.gender ? this.person.gender.id : null; }
},
civility: {
set(value) { this.person.civility = {id: value, type: 'chill_main_civility'}; },
@ -300,13 +300,13 @@ export default {
}
},
genderTranslation() {
switch (this.person.gender) {
switch (this.person.gender.genderTranslation) {
case 'woman':
return 'person.gender.woman';
case 'man':
return 'person.gender.man';
case 'both':
return 'person.gender.both';
case 'neutral':
return 'person.gender.neutral';
case 'unknown':
return 'person.gender.unknown';
default:
@ -334,6 +334,12 @@ export default {
this.config.civilities = civilities.results;
}
});
getGenders()
.then(genders => {
if ('results' in genders) {
this.config.genders = genders.results;
}
});
if (this.action !== 'create') {
this.loadData();
} else {

View File

@ -15,7 +15,7 @@ const personMessages = {
person: {
firstname: "Prénom",
lastname: "Nom",
born: (ctx: {gender: "man"|"woman"|"unknown"}) => {
born: (ctx: {gender: "man"|"woman"|"neutral"}) => {
if (ctx.gender === 'man') {
return 'Né le';
} else if (ctx.gender === 'woman') {
@ -36,7 +36,7 @@ const personMessages = {
placeholder: "Choisissez le genre de l'usager",
woman: "Féminin",
man: "Masculin",
both: "Neutre, non binaire",
neutral: "Neutre, non binaire",
unknown: "Non renseigné",
undefined: "Non renseigné"
},

View File

@ -33,7 +33,7 @@
{% if w.referrers %}
<li>
<span class="item-key">{{ 'Referrers'|trans ~ ' : ' }}</span>
{% for rh in w.referrersHistory %}
{% for rh in w.referrersHistoryCurrent %}
<span class="badge-user">{{ rh.user|chill_entity_render_box({'at_date': rh.startDate}) }}</span>
{% endfor %}
{% if w.referrers|length == 0 %}

View File

@ -85,13 +85,8 @@
{%- endif -%}
</div>
{%- if options['addInfo'] -%}
{% set gender = (person.gender == 'woman') ? 'fa-venus' :
(person.gender == 'man') ? 'fa-mars' : (person.gender == 'both') ? 'fa-neuter' : 'fa-genderless' %}
{% set genderTitle = (person.gender == 'woman') ? 'woman' :
(person.gender == 'man') ? 'man' : (person.gender == 'both') ? 'both' : 'Not given'|trans %}
<p class="moreinfo">
<i class="fa fa-fw {{ gender }}" title="{{ genderTitle|trans }}"></i>
{% if person.gender is not null %}{{ person.gender.icon|chill_entity_render_box }}{% endif %}
{%- if person.deathdate is not null -%}
{%- if person.birthdate is not null -%}
{# must be on one line to avoid spaces with dash #}
@ -106,7 +101,7 @@
{%- endif -%}
{%- elseif person.birthdate is not null -%}
<time datetime="{{ person.birthdate|date('Y-m-d') }}" title="{{ 'Birthdate'|trans }}">
{{ 'Born the date'|trans({'gender': person.gender,
{{ 'Born the date'|trans({'gender': person.gender ? person.gender.genderTranslation.value : 'neutral',
'birthdate': person.birthdate|format_date("medium") }) }}
</time>
{%- if options['addAge'] -%}

View File

@ -45,7 +45,7 @@
<div class="ms-auto">
{% if acp.requestoranonymous == false and acp.requestorPerson is same as(person) %}
<span class="as-requestor badge bg-info" title="{{ 'Requestor'|trans|e('html_attr') }}">
{{ 'Requestor'|trans({'gender': person.gender}) }}
{{ 'Requestor'|trans({'gender': person.gender ? person.gender.genderTranslation.value : 'neutral'}) }}
</span>
{% endif %}
@ -119,7 +119,7 @@
{% endif %}
{% endfor %}
{% if participating %}
{{ 'person.and_himself'|trans({'gender': person.gender}) }}
{{ 'person.And himself'|trans({'gender': person.gender ? person.gender.genderTranslation.value : 'neutral'}) }}
{% endif %}
</div>
</div>
@ -131,7 +131,7 @@
<div class="wl-col title">
<h3>
{% if acp.requestorPerson is not null %}
{{ 'Requestor'|trans({'gender': acp.requestorPerson.gender}) }}
{{ 'Requestor'|trans({'gender': acp.requestorPerson.gender ? person.gender.genderTranslation.value : 'neutral'}) }}
{% else %}
{{ 'Requestor'|trans({'gender': 'other'})}}
{% endif %}

View File

@ -73,8 +73,11 @@ This view should receive those arguments:
{% endfor %}
<dt>{{ 'Gender'|trans }}&nbsp;:</dt>
<dd>{{ ( person.gender|default('Not given'))|trans }}</dd>
{% if person.gender %}
<dd>{{ ( person.gender.label|localize_translatable_string ) }}</dd>
{% else %}
<dd>{{ 'gender.not defined'|trans }}</dd>
{% endif %}
</dl>
</figure>
</div>
@ -253,7 +256,6 @@ This view should receive those arguments:
<dd>{% if el.description is not empty %}{{ el.description }}&nbsp;:&nbsp;{% endif %}<a href="tel:{{ el.phonenumber|phone_number_format('E164') }}">{{ el.phonenumber|chill_format_phonenumber }}</a></dd>
{% endif %}
{% endfor %}
</ul>
</dl>
{% endif %}
{% endif %}

View File

@ -2,7 +2,7 @@
<ul>
<li><b>{{ 'gender'|trans }}</b>:
{{ person.gender|trans }}</li>
{{ person.gender.label|localize_translatable_string }}</li>
<li><b>{{ 'maritalStatus'|trans }}</b>:
{% if person.maritalStatus %}{{ person.maritalStatus.name|localize_translatable_string }}{% endif %}</li>
<li><b>{{ 'birthdate'|trans }}</b>:

View File

@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Search;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\GenderRepository;
use Chill\MainBundle\Search\AbstractSearch;
use Chill\MainBundle\Search\HasAdvancedSearchFormInterface;
use Chill\MainBundle\Search\ParsingException;
@ -21,7 +22,7 @@ use Chill\MainBundle\Search\SearchInterface;
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\Type\GenderType;
use Chill\PersonBundle\Form\Type\PickGenderType;
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
use libphonenumber\PhoneNumber;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@ -36,7 +37,14 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
'birthdate-after', 'gender', 'nationality', 'phonenumber', 'city',
];
public function __construct(private readonly \Twig\Environment $templating, private readonly ExtractDateFromPattern $extractDateFromPattern, private readonly ExtractPhonenumberFromPattern $extractPhonenumberFromPattern, private readonly PaginatorFactory $paginatorFactory, private readonly PersonACLAwareRepositoryInterface $personACLAwareRepository) {}
public function __construct(
private readonly \Twig\Environment $templating,
private readonly ExtractDateFromPattern $extractDateFromPattern,
private readonly ExtractPhonenumberFromPattern $extractPhonenumberFromPattern,
private readonly PaginatorFactory $paginatorFactory,
private readonly PersonACLAwareRepositoryInterface $personACLAwareRepository,
private readonly GenderRepository $genderRepository,
) {}
public function buildForm(FormBuilderInterface $builder)
{
@ -69,7 +77,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
'required' => false,
'label' => 'Part of the phonenumber',
])
->add('gender', GenderType::class, [
->add('gender', PickGenderType::class, [
'label' => 'Gender',
'required' => false,
'expanded' => false,
@ -87,7 +95,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
$string .= !isset($data['_default']) ? '' : $data['_default'].' ';
foreach (['firstname', 'lastname', 'gender', 'city'] as $key) {
foreach (['firstname', 'lastname', 'city'] as $key) {
$string .= !isset($data[$key]) ? '' : $key.':'.
// add quote if contains spaces
(str_contains((string) $data[$key], ' ') ? '"'.$data[$key].'"' : $data[$key])
@ -103,6 +111,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
$string .= !isset($data['phonenumber']) ? '' : 'phonenumber:'.$data['phonenumber']->getNationalNumber();
$string .= !isset($data['gender']) ? '' : 'gender:"'.$data['gender']->getId().'"';
return $string;
}
@ -110,7 +120,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
{
$data = [];
foreach (['firstname', 'lastname', 'gender', '_default', 'phonenumber', 'city'] as $key) {
foreach (['firstname', 'lastname', '_default', 'phonenumber', 'city'] as $key) {
$data[$key] = $terms[$key] ?? null;
}
@ -137,6 +147,10 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
$data['phonenumber'] = $phonenumber;
}
if (array_key_exists('gender', $terms)) {
$data['gender'] = $this->genderRepository->find((int) $terms['gender']);
}
return $data;
}
@ -256,7 +270,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
$birthdate,
$birthdateBefore,
$birthdateAfter,
$gender,
null !== $gender ? (int) $gender : null,
$countryCode,
$phonenumber,
$city
@ -316,7 +330,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
$birthdate,
$birthdateBefore,
$birthdateAfter,
$gender,
null !== $gender ? (int) $gender : null,
$countryCode,
$phonenumber,
$city

View File

@ -14,7 +14,7 @@ namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter;
@ -25,7 +25,7 @@ class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends Abstract
public function __construct(
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@ -16,6 +16,7 @@ use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
@ -30,7 +31,6 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Contracts\Translation\TranslatorInterface;
class PersonDocGenNormalizer implements
ContextAwareNormalizerInterface,
@ -40,7 +40,7 @@ class PersonDocGenNormalizer implements
private const CIRCULAR_KEY = 'person:circular';
public function __construct(private readonly PersonRenderInterface $personRender, private readonly RelationshipRepository $relationshipRepository, private readonly TranslatorInterface $translator, private readonly TranslatableStringHelper $translatableStringHelper, private readonly SummaryBudgetInterface $summaryBudget) {}
public function __construct(private readonly PersonRenderInterface $personRender, private readonly RelationshipRepository $relationshipRepository, private readonly TranslatableStringHelper $translatableStringHelper, private readonly SummaryBudgetInterface $summaryBudget) {}
public function normalize($person, $format = null, array $context = [])
{
@ -67,6 +67,7 @@ class PersonDocGenNormalizer implements
// when a person reference the same person... take care of circular references
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => fn ($object, $format, $context) => $this->normalizer->normalize(null, $format, $context),
]);
$genderContext = array_merge($context, ['docgen:expects' => Gender::class]);
if (null === $person) {
return $this->normalizeNullValue($format, $context);
@ -94,7 +95,7 @@ class PersonDocGenNormalizer implements
'age' => (int) $person->getAge(),
'birthdate' => $this->normalizer->normalize($person->getBirthdate(), $format, $dateContext),
'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $dateContext),
'gender' => $this->translator->trans($person->getGender()),
'gender' => $this->normalizer->normalize($person->getGender(), $format, $genderContext),
'maritalStatus' => null !== ($ms = $person->getMaritalStatus()) ? $this->translatableStringHelper->localize($ms->getName()) : '',
'maritalStatusDate' => $this->normalizer->normalize($person->getMaritalStatusDate(), $format, $dateContext),
'maritalStatusComment' => $this->normalizer->normalize($person->getMaritalStatusComment(), $format, $dateContext),

View File

@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
@ -112,7 +113,9 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
break;
case 'gender':
$person->setGender($data[$item]);
$gender = $this->denormalizer->denormalize($data[$item], Gender::class, $format, []);
$person->setGender($gender);
break;
@ -199,7 +202,7 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
'phonenumber' => $this->normalizer->normalize($person->getPhonenumber(), $format, $context),
'mobilenumber' => $this->normalizer->normalize($person->getMobilenumber(), $format, $context),
'email' => $person->getEmail(),
'gender' => $person->getGender(),
'gender' => $this->normalizer->normalize($person->getGender(), $format, $context),
'civility' => $this->normalizer->normalize($person->getCivility(), $format, $context),
];

View File

@ -11,6 +11,9 @@ declare(strict_types=1);
namespace Tests\Controller\AccompanyingCoursWorkApiController;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Entity\GenderIconEnum;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Test\PrepareClientTrait;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
@ -129,8 +132,15 @@ class ConflictTest extends WebTestCase
$period = new AccompanyingPeriod();
$em->persist($period);
$gender = new Gender();
$gender->setGenderTranslation(GenderEnum::MALE);
$gender->setLabel(['fr' => 'homme']);
$gender->setIcon(GenderIconEnum::MALE);
$em->persist($gender);
$period->addPerson(($p = new Person())->setFirstName('test')->setLastName('test')
->setBirthdate(new \DateTime('1980-01-01'))->setGender(Person::BOTH_GENDER));
->setBirthdate(new \DateTime('1980-01-01'))->setGender($gender));
$em->persist($p);
$issue = (new SocialIssue())->setTitle(['fr' => 'test']);
$em->persist($issue);

View File

@ -14,6 +14,9 @@ namespace Chill\PersonBundle\Tests\Controller;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\AddressReference;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Entity\GenderIconEnum;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Test\PrepareClientTrait;
use Chill\PersonBundle\Entity\Household\Household;
@ -51,9 +54,15 @@ final class HouseholdApiControllerTest extends WebTestCase
->setMaxResults(1)
->getQuery()->getSingleResult();
$gender = new Gender();
$gender->setGenderTranslation(GenderEnum::MALE);
$gender->setLabel(['fr' => 'homme']);
$gender->setIcon(GenderIconEnum::MALE);
$em->persist($gender);
$p = new Person();
$p->setFirstname('test')->setLastName('test lastname')
->setGender(Person::BOTH_GENDER)
->setGender($gender)
->setCenter($centerA);
$em->persist($p);
$h = new Household();

View File

@ -112,24 +112,24 @@ final class PersonControllerCreateTest extends WebTestCase
$genderType = $form->get(self::GENDER_INPUT);
$this->assertEquals(
'radio',
'select',
$genderType->getType(),
'The gender input has radio buttons'
'The gender input is a select form to select a gender entity'
);
$this->assertEquals(
3,
\count($genderType->availableOptionValues()),
'The gender input has three options: man, women and undefined'
);
$this->assertTrue(
\in_array('man', $genderType->availableOptionValues(), true),
'gender has "homme" option'
);
$this->assertTrue(
\in_array('woman', $genderType->availableOptionValues(), true),
'gender has "femme" option'
);
$this->assertFalse($genderType->hasValue(), 'The gender input is not checked');
/* $this->assertEquals(
3,
\count($genderType->availableOptionValues()),
'The gender input has three options: man, women and undefined'
);
$this->assertTrue(
\in_array('man', $genderType->availableOptionValues(), true),
'gender has "homme" option'
);
$this->assertTrue(
\in_array('woman', $genderType->availableOptionValues(), true),
'gender has "femme" option'
);
$this->assertFalse($genderType->hasValue(), 'The gender input is not checked');*/
return $form;
}
@ -226,7 +226,8 @@ final class PersonControllerCreateTest extends WebTestCase
) {
$creationForm->get(self::FIRSTNAME_INPUT)->setValue($firstname.'_'.uniqid());
$creationForm->get(self::LASTNAME_INPUT)->setValue($lastname.'_'.uniqid());
$creationForm->get(self::GENDER_INPUT)->select('man');
// Todo change hardcoded id
$creationForm->get(self::GENDER_INPUT)->select(5);
$date = $birthdate ?? new \DateTime('1947-02-01');
$creationForm->get(self::BIRTHDATE_INPUT)->setValue($date->format('Y-m-d'));

View File

@ -12,6 +12,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Tests\Controller;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Entity\GenderIconEnum;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Test\PrepareClientTrait;
use Chill\PersonBundle\Entity\Person;
@ -160,12 +163,18 @@ final class PersonControllerUpdateTest extends WebTestCase
$em = self::getContainer()->get(EntityManagerInterface::class);
$center = $centerRepository->findOneBy(['name' => 'Center A']);
$gender = new Gender();
$gender->setGenderTranslation(GenderEnum::MALE);
$gender->setLabel(['fr' => 'homme']);
$gender->setIcon(GenderIconEnum::MALE);
$em->persist($gender);
$person = new Person();
$person
->setFirstName('Foo')
->setLastName('Bar')
->setBirthdate(new \DateTime('2017-09-30'))
->setGender(Person::MALE_GENDER)
->setGender($gender)
->setCenter($center);
$em->persist($person);

View File

@ -11,6 +11,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Tests\Controller;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Entity\GenderIconEnum;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Test\PrepareClientTrait;
use Chill\PersonBundle\Entity\Person;
@ -99,12 +102,18 @@ final class PersonControllerViewTest extends WebTestCase
$em = self::getContainer()->get(EntityManagerInterface::class);
$center = $centerRepository->findOneBy(['name' => 'Center A']);
$gender = new Gender();
$gender->setGenderTranslation(GenderEnum::MALE);
$gender->setLabel(['fr' => 'homme']);
$gender->setIcon(GenderIconEnum::MALE);
$em->persist($gender);
$person = new Person();
$person
->setFirstName('Foo')
->setLastName('Bar')
->setBirthdate(new \DateTime('2017-09-30'))
->setGender(Person::MALE_GENDER)
->setGender($gender)
->setCenter($center);
$em->persist($person);

View File

@ -41,13 +41,13 @@ final class GenderFilterTest extends AbstractFilterTest
{
return [
[
'accepted_genders' => [Person::FEMALE_GENDER],
'accepted_genders' => ['man'],
],
[
'accepted_genders' => [Person::MALE_GENDER],
'accepted_genders' => ['woman'],
],
[
'accepted_genders' => [Person::MALE_GENDER, Person::BOTH_GENDER],
'accepted_genders' => ['man', 'both'],
],
];
}

View File

@ -257,7 +257,7 @@ final class PersonDocGenNormalizerTest extends KernelTestCase
$personDocGenNormalizer = new PersonDocGenNormalizer(
$personRender ?? self::getContainer()->get(PersonRender::class),
$relationshipRepository ?? self::getContainer()->get(RelationshipRepository::class),
$translator ?? self::getContainer()->get(TranslatorInterface::class),
// $translator ?? self::getContainer()->get(TranslatorInterface::class),
$translatableStringHelper ?? self::getContainer()->get(TranslatableStringHelperInterface::class),
$summaryBudget->reveal(),
);
@ -299,7 +299,7 @@ final class PersonDocGenNormalizerTest extends KernelTestCase
$normalizer = new PersonDocGenNormalizer(
$personRender ?? self::getContainer()->get(PersonRender::class),
$relationshipRepository,
$translator ?? self::getContainer()->get(TranslatorInterface::class),
// $translator ?? self::getContainer()->get(TranslatorInterface::class),
$translatableStringHelper ?? self::getContainer()->get(TranslatableStringHelperInterface::class),
$summaryBudget->reveal()
);

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\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20240926100337 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add foreign key gender property to person and transfer values';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_person ADD gender_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_person_person ADD CONSTRAINT FK_BF210A14708A0E0 FOREIGN KEY (gender_id) REFERENCES chill_main_gender (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_BF210A14708A0E0 ON chill_person_person (gender_id)');
// transfer gender values to point to corresponding gender entity within new column
$this->addSql("
UPDATE chill_person_person AS p
SET gender_id = g.id
FROM chill_main_gender AS g
WHERE g.genderTranslation = p.gender AND p.gender IN ('man', 'woman', 'unknown')
OR (g.genderTranslation = 'neutral' AND p.gender = 'both')
");
// delete old gender column
$this->addSql('ALTER TABLE chill_person_person DROP gender');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_person ADD gender VARCHAR(9) DEFAULT NULL');
$this->addSql('ALTER TABLE chill_person_person DROP gender_id');
}
}

View File

@ -2,6 +2,7 @@ Born the date: >-
{gender, select,
man {Né le {birthdate}}
woman {Née le {birthdate}}
neutral {Né·e le {birthdate}}
other {Né·e le {birthdate}}
}
@ -9,17 +10,18 @@ Requestor: >-
{gender, select,
man {Demandeur}
woman {Demandeuse}
other {Demandeur·euse}
neutral {Demandeur·euse}
}
person:
and_himself: >-
from_the: depuis le
And himself: >-
{gender, select,
man {et lui-même}
woman {et elle-même}
neutral {et lui·elle-même}
other {et lui·elle-même}
}
from_the: depuis le
household:
Household: Ménage

View File

@ -64,6 +64,7 @@ Female: Femme
#Both: Neutre
man: Homme
woman: Femme
neutral: Neutre
#both: Neutre
Man: Homme
Woman: Femme
@ -1192,6 +1193,8 @@ export:
date_after: Après le
date_before: Avant le
title: Filtrer les usagers n'ayant été associés à aucun parcours
gender:
no_gender: genre non specifié
course:
not_having_address_reference: