mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-20 14:43:49 +00:00
Merge remote-tracking branch 'origin/master' into issue294_suggestPersonInActivity
This commit is contained in:
@@ -298,16 +298,19 @@ final class ActivityController extends AbstractController
|
||||
|
||||
$activity_array = $this->serializer->normalize($entity, 'json', ['groups' => 'read']);
|
||||
|
||||
$defaultLocationId = $this->getUser()->getCurrentLocation()->getId();
|
||||
|
||||
return $this->render($view, [
|
||||
'person' => $person,
|
||||
'accompanyingCourse' => $accompanyingPeriod,
|
||||
'entity' => $entity,
|
||||
'form' => $form->createView(),
|
||||
'activity_json' => $activity_array
|
||||
'activity_json' => $activity_array,
|
||||
'default_location_id' => $defaultLocationId
|
||||
]);
|
||||
}
|
||||
|
||||
public function showAction(Request $request, $id): Response
|
||||
public function showAction(Request $request, int $id): Response
|
||||
{
|
||||
$view = null;
|
||||
|
||||
@@ -361,7 +364,7 @@ final class ActivityController extends AbstractController
|
||||
/**
|
||||
* Displays a form to edit an existing Activity entity.
|
||||
*/
|
||||
public function editAction($id, Request $request): Response
|
||||
public function editAction(int $id, Request $request): Response
|
||||
{
|
||||
$view = null;
|
||||
|
||||
|
@@ -36,6 +36,8 @@ use Chill\MainBundle\DataFixtures\ORM\LoadScopes;
|
||||
|
||||
class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
use \Symfony\Component\DependencyInjection\ContainerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var \Faker\Generator
|
||||
*/
|
||||
@@ -80,15 +82,10 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
|
||||
*
|
||||
* @return \Chill\ActivityBundle\Entity\ActivityReason
|
||||
*/
|
||||
private function getRandomActivityReason(array $excludingIds)
|
||||
private function getRandomActivityReason()
|
||||
{
|
||||
$reasonRef = LoadActivityReason::$references[array_rand(LoadActivityReason::$references)];
|
||||
|
||||
if (in_array($this->getReference($reasonRef)->getId(), $excludingIds, true)) {
|
||||
// we have a reason which should be excluded. Find another...
|
||||
return $this->getRandomActivityReason($excludingIds);
|
||||
}
|
||||
|
||||
return $this->getReference($reasonRef);
|
||||
}
|
||||
|
||||
@@ -103,7 +100,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
|
||||
return $this->getReference($userRef);
|
||||
}
|
||||
|
||||
public function newRandomActivity($person)
|
||||
public function newRandomActivity($person): ?Activity
|
||||
{
|
||||
$activity = (new Activity())
|
||||
->setUser($this->getRandomUser())
|
||||
@@ -116,11 +113,13 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
|
||||
|
||||
// ->setAttendee($this->faker->boolean())
|
||||
|
||||
$usedId = array();
|
||||
for ($i = 0; $i < rand(0, 4); $i++) {
|
||||
$reason = $this->getRandomActivityReason($usedId);
|
||||
$usedId[] = $reason->getId();
|
||||
$activity->addReason($reason);
|
||||
$reason = $this->getRandomActivityReason();
|
||||
if (null !== $reason) {
|
||||
$activity->addReason($reason);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $activity;
|
||||
@@ -137,7 +136,9 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
|
||||
|
||||
for ($i = 0; $i < $activityNbr; $i ++) {
|
||||
$activity = $this->newRandomActivity($person);
|
||||
$manager->persist($activity);
|
||||
if (null !== $activity) {
|
||||
$manager->persist($activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
$manager->flush();
|
||||
|
@@ -10,9 +10,12 @@
|
||||
</persons-bloc>
|
||||
</div>
|
||||
<div v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0">
|
||||
<ul>
|
||||
<ul class="list-unstyled">
|
||||
<li v-for="p in suggestedEntities" @click="addSuggestedEntity(p)">
|
||||
{{ p.text }}
|
||||
<span class="badge bg-primary" style="cursor: pointer;">
|
||||
<i class="fa fa-plus fa-fw text-success"></i>
|
||||
{{ p.text }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<span class="chill_denomination">
|
||||
{{ textCutted }}
|
||||
</span>
|
||||
<a class="fa fa-fw fa-times"
|
||||
<a class="fa fa-fw fa-times text-danger text-decoration-none"
|
||||
@click.prevent="$emit('remove', person)">
|
||||
</a>
|
||||
</span>
|
||||
|
@@ -55,16 +55,17 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getLocationsList();
|
||||
getLocations().then(response => new Promise(resolve => {
|
||||
console.log('getLocations', response);
|
||||
this.locations = response.results;
|
||||
if (window.default_location_id) {
|
||||
let location = this.locations.filter(l => l.id === window.default_location_id);
|
||||
this.$store.dispatch('updateLocation', location);
|
||||
}
|
||||
resolve();
|
||||
}))
|
||||
},
|
||||
methods: {
|
||||
getLocationsList() {
|
||||
getLocations().then(response => new Promise(resolve => {
|
||||
console.log('getLocations', response);
|
||||
this.locations = response.results;
|
||||
resolve();
|
||||
}))
|
||||
},
|
||||
customLabel(value) {
|
||||
return `${value.locationType.title.fr} ${value.name ? value.name : ''}`;
|
||||
}
|
||||
|
@@ -55,6 +55,9 @@ const store = createStore({
|
||||
.filter((p) => !existingPersonIds.includes(p.id));
|
||||
},
|
||||
suggestedRequestor(state) {
|
||||
if (state.activity.accompanyingPeriod.requestor === null) {
|
||||
return [];
|
||||
}
|
||||
const existingPersonIds = state.activity.persons.map((p) => p.id);
|
||||
const existingThirdPartyIds = state.activity.thirdParties.map(
|
||||
(p) => p.id
|
||||
@@ -74,7 +77,7 @@ const store = createStore({
|
||||
return state.activity.activityType.usersVisible === 0
|
||||
? []
|
||||
: [state.activity.accompanyingPeriod.user].filter(
|
||||
(u) => !existingUserIds.includes(u.id)
|
||||
(u) => u !== null && !existingUserIds.includes(u.id)
|
||||
);
|
||||
},
|
||||
suggestedResources(state) {
|
||||
|
@@ -27,14 +27,16 @@
|
||||
{{ activity.type.name | localize_translatable_string }}
|
||||
|
||||
<ul class="small_in_title">
|
||||
{% if activity.location and t.locationVisible %}
|
||||
<li>
|
||||
<abbr title="{{ 'location'|trans }}">{{ 'location'|trans ~ ': ' }}</abbr>
|
||||
{# TODO {% if activity.location %}{{ activity.location }}{% endif %} #}
|
||||
Domicile de l'usager
|
||||
<span class="item-key">{{ 'location'|trans ~ ': ' }}</span>
|
||||
<span>{{ activity.location.locationType.title|localize_translatable_string }}</span>
|
||||
{{ activity.location.name }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if activity.user and t.userVisible %}
|
||||
<li>
|
||||
<abbr title="{{ 'Referrer'|trans }}">{{ 'Referrer'|trans ~ ': ' }}</abbr>
|
||||
<span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span>
|
||||
{{ activity.user.usernameCanonical }}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
@@ -22,6 +22,7 @@
|
||||
'{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}');
|
||||
});
|
||||
window.activity = {{ activity_json|json_encode|raw }};
|
||||
window.default_location_id = {{ default_location_id }};
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_activity') }}
|
||||
{% endblock %}
|
||||
|
@@ -55,7 +55,7 @@
|
||||
{% endif %}
|
||||
|
||||
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
|
||||
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'bloc' } %}
|
||||
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'bloc', 'badge_person': 'true' } %}
|
||||
|
||||
<h2 class="chill-red">{{ 'Activity data'|trans }}</h2>
|
||||
|
||||
|
@@ -1,4 +1,2 @@
|
||||
|
||||
{{ dump(notification) }}
|
||||
|
||||
<a href="{{ path('chill_activity_activity_show', {'id': notification.relatedEntityId }) }}">Go to Activity</a>
|
||||
|
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Activity;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211119173555 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove comment on deprecated json_array type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$columns = [
|
||||
'activitytype.name',
|
||||
'activitytypecategory.name'
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$this->addSql("COMMENT ON COLUMN $col IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Calendar;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211119173557 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove comment on deprecated json_array type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$columns = [
|
||||
'chill_calendar.cancel_reason.name',
|
||||
'chill_calendar.invite.status',
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$this->addSql("COMMENT ON COLUMN $col IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Serializer\Encoder;
|
||||
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
|
||||
class DocGenEncoder implements \Symfony\Component\Serializer\Encoder\EncoderInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function encode($data, string $format, array $context = [])
|
||||
{
|
||||
if (!$this->isAssociative($data)) {
|
||||
throw new UnexpectedValueException("Only associative arrays are allowed; lists are not allowed");
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$this->recusiveEncoding($data, $result, '');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function recusiveEncoding(array $data, array &$result, $path)
|
||||
{
|
||||
if ($this->isAssociative($data)) {
|
||||
foreach ($data as $key => $value) {
|
||||
if (\is_array($value)) {
|
||||
$this->recusiveEncoding($value, $result, $this->canonicalizeKey($path, $key));
|
||||
} else {
|
||||
$result[$this->canonicalizeKey($path, $key)] = $value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($data as $elem) {
|
||||
|
||||
if (!$this->isAssociative($elem)) {
|
||||
throw new UnexpectedValueException(sprintf("Embedded loops are not allowed. See data under %s path", $path));
|
||||
}
|
||||
|
||||
$sub = [];
|
||||
$this->recusiveEncoding($elem, $sub, '');
|
||||
$result[$path][] = $sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function canonicalizeKey(string $path, string $key): string
|
||||
{
|
||||
return $path === '' ? $key : $path.'_'.$key;
|
||||
}
|
||||
|
||||
private function isAssociative(array $data)
|
||||
{
|
||||
$keys = \array_keys($data);
|
||||
|
||||
return $keys !== \array_keys($keys);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supportsEncoding(string $format)
|
||||
{
|
||||
return $format === 'docgen';
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Serializer\Helper;
|
||||
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class NormalizeNullValueHelper
|
||||
{
|
||||
private NormalizerInterface $normalizer;
|
||||
|
||||
public function __construct(NormalizerInterface $normalizer)
|
||||
{
|
||||
$this->normalizer = $normalizer;
|
||||
}
|
||||
|
||||
public function normalize(array $attributes, string $format = 'docgen', ?array $context = [])
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($attributes as $key => $class) {
|
||||
if (is_numeric($key)) {
|
||||
$data[$class] = '';
|
||||
} else {
|
||||
switch ($class) {
|
||||
case 'array':
|
||||
case 'bool':
|
||||
case 'double':
|
||||
case 'float':
|
||||
case 'int':
|
||||
case 'resource':
|
||||
case 'string':
|
||||
case 'null':
|
||||
$data[$key] = '';
|
||||
break;
|
||||
default:
|
||||
$data[$key] = $this->normalizer->normalize(null, $format, \array_merge(
|
||||
$context,
|
||||
['docgen:expects' => $class]
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessor;
|
||||
use Symfony\Component\Serializer\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Serializer\Exception\LogicException;
|
||||
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
|
||||
use Symfony\Component\Serializer\Mapping\ClassMetadata;
|
||||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class DocGenObjectNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
private ClassMetadataFactoryInterface $classMetadataFactory;
|
||||
private PropertyAccessor $propertyAccess;
|
||||
|
||||
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory)
|
||||
{
|
||||
$this->classMetadataFactory = $classMetadataFactory;
|
||||
$this->propertyAccess = PropertyAccess::createPropertyAccessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function normalize($object, string $format = null, array $context = [])
|
||||
{
|
||||
$classMetadataKey = $object ?? $context['docgen:expects'];
|
||||
|
||||
if (!$this->classMetadataFactory->hasMetadataFor($classMetadataKey)) {
|
||||
throw new LogicException(sprintf("This object does not have metadata: %s. Add groups on this entity to allow to serialize with the format %s and groups %s", is_object($object) ? get_class($object) : $context['docgen:expects'], $format, \implode(', ', $context['groups'])));
|
||||
}
|
||||
|
||||
$metadata = $this->classMetadataFactory->getMetadataFor($classMetadataKey);
|
||||
$expectedGroups = \array_key_exists(AbstractNormalizer::GROUPS, $context) ?
|
||||
\is_array($context[AbstractNormalizer::GROUPS]) ? $context[AbstractNormalizer::GROUPS] : [$context[AbstractNormalizer::GROUPS]]
|
||||
: [];
|
||||
$attributes = \array_filter(
|
||||
$metadata->getAttributesMetadata(),
|
||||
function (AttributeMetadata $a) use ($expectedGroups) {
|
||||
foreach ($a->getGroups() as $g) {
|
||||
if (\in_array($g, $expectedGroups, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (null === $object) {
|
||||
return $this->normalizeNullData($format, $context, $metadata, $attributes);
|
||||
}
|
||||
|
||||
return $this->normalizeObject($object, $format, $context, $expectedGroups, $metadata, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $format
|
||||
* @param array $context
|
||||
* @param array $expectedGroups
|
||||
* @param ClassMetadata $metadata
|
||||
* @param array|AttributeMetadata[] $attributes
|
||||
*/
|
||||
private function normalizeNullData(string $format, array $context, ClassMetadata $metadata, array $attributes): array
|
||||
{
|
||||
$keys = [];
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$key = $attribute->getSerializedName() ?? $attribute->getName();
|
||||
$keys[$key] = $this->getExpectedType($attribute, $metadata->getReflectionClass());
|
||||
}
|
||||
|
||||
$normalizer = new NormalizeNullValueHelper($this->normalizer);
|
||||
|
||||
return $normalizer->normalize($keys, $format, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $object
|
||||
* @param $format
|
||||
* @param array $context
|
||||
* @param array $expectedGroups
|
||||
* @param ClassMetadata $metadata
|
||||
* @param array|AttributeMetadata[] $attributes
|
||||
* @return array
|
||||
* @throws ExceptionInterface
|
||||
*/
|
||||
private function normalizeObject($object, $format, array $context, array $expectedGroups, ClassMetadata $metadata, array $attributes)
|
||||
{
|
||||
$data = [];
|
||||
$reflection = $metadata->getReflectionClass();
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var AttributeMetadata $attribute */
|
||||
$value = $this->propertyAccess->getValue($object, $attribute->getName());
|
||||
$key = $attribute->getSerializedName() ?? $attribute->getName();
|
||||
|
||||
if (is_object($value)) {
|
||||
$data[$key] =
|
||||
$this->normalizer->normalize($value, $format, \array_merge(
|
||||
$context, $attribute->getNormalizationContextForGroups($expectedGroups)
|
||||
));
|
||||
} elseif (null === $value) {
|
||||
$data[$key] = $this->normalizeNullOutputValue($format, $context, $attribute, $reflection);
|
||||
} else {
|
||||
$data[$key] = (string) $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getExpectedType(AttributeMetadata $attribute, \ReflectionClass $reflection): string
|
||||
{
|
||||
// we have to get the expected content
|
||||
if ($reflection->hasProperty($attribute->getName())) {
|
||||
$type = $reflection->getProperty($attribute->getName())->getType();
|
||||
} elseif ($reflection->hasMethod($attribute->getName())) {
|
||||
$type = $reflection->getMethod($attribute->getName())->getReturnType();
|
||||
} else {
|
||||
throw new \LogicException(sprintf(
|
||||
"Could not determine how the content is determined for the attribute %s. Add attribute property only on property or method", $attribute->getName()
|
||||
));
|
||||
}
|
||||
|
||||
if (null === $type) {
|
||||
throw new \LogicException(sprintf(
|
||||
"Could not determine the type for this attribute: %s. Add a return type to the method or property declaration", $attribute->getName()
|
||||
));
|
||||
}
|
||||
|
||||
return $type->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
private function normalizeNullOutputValue($format, array $context, AttributeMetadata $attribute, \ReflectionClass $reflection)
|
||||
{
|
||||
$type = $this->getExpectedType($attribute, $reflection);
|
||||
|
||||
switch ($type) {
|
||||
case 'array':
|
||||
case 'bool':
|
||||
case 'double':
|
||||
case 'float':
|
||||
case 'int':
|
||||
case 'resource':
|
||||
case 'string':
|
||||
return '';
|
||||
default:
|
||||
return $this->normalizer->normalize(
|
||||
null,
|
||||
$format,
|
||||
\array_merge(
|
||||
$context,
|
||||
['docgen:expects' => $type]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supportsNormalization($data, string $format = null): bool
|
||||
{
|
||||
return $format === 'docgen' && (is_object($data) || null === $data);
|
||||
}
|
||||
}
|
@@ -8,3 +8,10 @@ services:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Repository/'
|
||||
|
||||
Chill\DocGeneratorBundle\Serializer\Normalizer\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Serializer/Normalizer/'
|
||||
tags:
|
||||
- { name: 'serializer.normalizer', priority: -152 }
|
||||
|
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\DocGenerator;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211119173556 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove comment on deprecated json_array type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$columns = [
|
||||
'chill_docgen_template.name'
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$this->addSql("COMMENT ON COLUMN $col IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Tests\Serializer\Encoder;
|
||||
|
||||
use Chill\DocGeneratorBundle\Serializer\Encoder\DocGenEncoder;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
|
||||
class DocGenEncoderTest extends TestCase
|
||||
{
|
||||
private DocGenEncoder $encoder;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->encoder = new DocGenEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider generateEncodeData
|
||||
*/
|
||||
public function testEncode($expected, $data, string $msg)
|
||||
{
|
||||
$generated = $this->encoder->encode($data, 'docgen');
|
||||
$this->assertEquals($expected, $generated, $msg);
|
||||
}
|
||||
|
||||
public function testEmbeddedLoopsThrowsException()
|
||||
{
|
||||
$this->expectException(UnexpectedValueException::class);
|
||||
|
||||
$data = [
|
||||
'data' => [
|
||||
['item' => 'one'],
|
||||
[
|
||||
'embedded' => [
|
||||
[
|
||||
['subitem' => 'two'],
|
||||
['subitem' => 'three']
|
||||
]
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
$this->encoder->encode($data, 'docgen');
|
||||
}
|
||||
|
||||
public function generateEncodeData()
|
||||
{
|
||||
yield [ ['tests' => 'ok'], ['tests' => 'ok'], "A simple test with a simple array"];
|
||||
|
||||
yield [
|
||||
// expected:
|
||||
['item_subitem' => 'value'],
|
||||
// data:
|
||||
['item' => ['subitem' => 'value']],
|
||||
"A test with multidimensional array"
|
||||
];
|
||||
|
||||
yield [
|
||||
// expected:
|
||||
[ 'data' => [['item' => 'one'], ['item' => 'two']] ],
|
||||
// data:
|
||||
[ 'data' => [['item' => 'one'], ['item' => 'two']] ],
|
||||
"a list of items"
|
||||
];
|
||||
|
||||
yield [
|
||||
// expected:
|
||||
[ 'data' => [['item_subitem' => 'alpha'], ['item' => 'two']] ],
|
||||
// data:
|
||||
[ 'data' => [['item' => ['subitem' => 'alpha']], ['item' => 'two'] ] ],
|
||||
"a list of items with multidimensional array inside item"
|
||||
];
|
||||
|
||||
yield [
|
||||
// expected:
|
||||
[
|
||||
'persons' => [
|
||||
[
|
||||
'firstname' => 'Jonathan',
|
||||
'lastname' => 'Dupont',
|
||||
'dateOfBirth_long' => '16 juin 1981',
|
||||
'dateOfBirth_short' => '16/06/1981',
|
||||
'father_firstname' => 'Marcel',
|
||||
'father_lastname' => 'Dupont',
|
||||
'father_dateOfBirth_long' => '10 novembre 1953',
|
||||
'father_dateOfBirth_short' => '10/11/1953'
|
||||
],
|
||||
]
|
||||
],
|
||||
// data:
|
||||
[
|
||||
'persons' => [
|
||||
[
|
||||
'firstname' => 'Jonathan',
|
||||
'lastname' => 'Dupont',
|
||||
'dateOfBirth' => [ 'long' => '16 juin 1981', 'short' => '16/06/1981'],
|
||||
'father' => [
|
||||
'firstname' => 'Marcel',
|
||||
'lastname' => 'Dupont',
|
||||
'dateOfBirth' => ['long' => '10 novembre 1953', 'short' => '10/11/1953']
|
||||
]
|
||||
],
|
||||
]
|
||||
],
|
||||
"a longer list, with near real data inside and embedded associative arrays"
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\DocGeneratorBundle\tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class DocGenObjectNormalizerTest extends KernelTestCase
|
||||
{
|
||||
private NormalizerInterface $normalizer;
|
||||
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
|
||||
$this->normalizer = self::$container->get(NormalizerInterface::class);
|
||||
}
|
||||
|
||||
public function testNormalizationBasic()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUsername('User Test');
|
||||
$user->setMainCenter($center = new Center());
|
||||
$center->setName('test');
|
||||
|
||||
$normalized = $this->normalizer->normalize($user, 'docgen', [ AbstractNormalizer::GROUPS => ['docgen:read']]);
|
||||
$expected = [
|
||||
'label' => 'User Test',
|
||||
'email' => '',
|
||||
'mainCenter' => [
|
||||
'name' => 'test'
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $normalized, "test normalization fo an user");
|
||||
}
|
||||
|
||||
public function testNormalizeWithNullValueEmbedded()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUsername('User Test');
|
||||
|
||||
$normalized = $this->normalizer->normalize($user, 'docgen', [ AbstractNormalizer::GROUPS => ['docgen:read']]);
|
||||
$expected = [
|
||||
'label' => 'User Test',
|
||||
'email' => '',
|
||||
'mainCenter' => [
|
||||
'name' => ''
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $normalized, "test normalization fo an user with null center");
|
||||
}
|
||||
|
||||
public function testNormalizeNullObjectWithObjectEmbedded()
|
||||
{
|
||||
$normalized = $this->normalizer->normalize(null, 'docgen', [
|
||||
AbstractNormalizer::GROUPS => ['docgen:read'],
|
||||
'docgen:expects' => User::class,
|
||||
]);
|
||||
|
||||
$expected = [
|
||||
'label' => '',
|
||||
'email' => '',
|
||||
'mainCenter' => [
|
||||
'name' => ''
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $normalized, "test normalization for a null user");
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -1,44 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2018 Champs-Libres SCRLFS
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Security\Authorization;
|
||||
|
||||
use App\Security\Authorization\VoterHelperFactory;
|
||||
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
|
||||
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||
use Chill\DocStoreBundle\Entity\PersonDocument;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class PersonDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
|
||||
{
|
||||
const CREATE = 'CHILL_PERSON_DOCUMENT_CREATE';
|
||||
|
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\DocStore;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211119173558 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove comment on deprecated json_array type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$columns = [
|
||||
'chill_doc.document_category.name',
|
||||
'chill_doc.stored_object.key',
|
||||
'chill_doc.stored_object.iv',
|
||||
'chill_doc.stored_object.datas',
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$this->addSql("COMMENT ON COLUMN $col IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@
|
||||
namespace Chill\MainBundle;
|
||||
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Chill\MainBundle\Search\SearchApiInterface;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Chill\MainBundle\Security\Authorization\ChillVoterInterface;
|
||||
use Chill\MainBundle\Security\ProvideRoleInterface;
|
||||
@@ -41,6 +42,8 @@ class ChillMainBundle extends Bundle
|
||||
->addTag('chill_main.scope_resolver');
|
||||
$container->registerForAutoconfiguration(ChillEntityRenderInterface::class)
|
||||
->addTag('chill.render_entity');
|
||||
$container->registerForAutoconfiguration(SearchApiInterface::class)
|
||||
->addTag('chill.search_api_provider');
|
||||
|
||||
$container->addCompilerPass(new SearchableServicesCompilerPass());
|
||||
$container->addCompilerPass(new ConfigConsistencyCompilerPass());
|
||||
|
@@ -18,6 +18,7 @@ use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\UserType;
|
||||
use Chill\MainBundle\Entity\GroupCenter;
|
||||
use Chill\MainBundle\Form\Type\ComposedGroupCenterType;
|
||||
use Chill\MainBundle\Form\UserCurrentLocationType;
|
||||
use Chill\MainBundle\Form\UserPasswordType;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
@@ -87,7 +88,7 @@ class UserController extends CRUDController
|
||||
[
|
||||
'add_groupcenter_form' => $this->createAddLinkGroupCenterForm($entity, $request)->createView(),
|
||||
'delete_groupcenter_form' => array_map(
|
||||
function(\Symfony\Component\Form\Form $form) {
|
||||
function (\Symfony\Component\Form\Form $form) {
|
||||
return $form->createView();
|
||||
},
|
||||
iterator_to_array($this->getDeleteLinkGroupCenterByUser($entity, $request), true)
|
||||
@@ -164,7 +165,7 @@ class UserController extends CRUDController
|
||||
}
|
||||
|
||||
$groupCenter = $em->getRepository('ChillMainBundle:GroupCenter')
|
||||
->find($gcid);
|
||||
->find($gcid);
|
||||
|
||||
if (!$groupCenter) {
|
||||
throw $this->createNotFoundException('Unable to find groupCenter entity');
|
||||
@@ -181,10 +182,9 @@ class UserController extends CRUDController
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $this->get('translator')
|
||||
->trans('The permissions where removed.'));
|
||||
->trans('The permissions where removed.'));
|
||||
|
||||
return $this->redirect($this->generateUrl('chill_crud_admin_user_edit', array('id' => $uid)));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,24 +206,26 @@ class UserController extends CRUDController
|
||||
|
||||
if ($form->isValid()) {
|
||||
$groupCenter = $this->getPersistedGroupCenter(
|
||||
$form[self::FORM_GROUP_CENTER_COMPOSED]->getData());
|
||||
$form[self::FORM_GROUP_CENTER_COMPOSED]->getData()
|
||||
);
|
||||
$user->addGroupCenter($groupCenter);
|
||||
|
||||
if ($this->validator->validate($user)->count() === 0) {
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $this->get('translator')->trans('The '
|
||||
$this->addFlash('success', $this->get('translator')->trans('The '
|
||||
. 'permissions have been successfully added to the user'));
|
||||
|
||||
$returnPathParams = $request->query->has('returnPath') ?
|
||||
['returnPath' => $request->query->get('returnPath')] : [];
|
||||
|
||||
return $this->redirect($this->generateUrl('chill_crud_admin_user_edit',
|
||||
\array_merge(['id' => $uid], $returnPathParams)));
|
||||
$returnPathParams = $request->query->has('returnPath') ?
|
||||
['returnPath' => $request->query->get('returnPath')] : [];
|
||||
|
||||
return $this->redirect($this->generateUrl(
|
||||
'chill_crud_admin_user_edit',
|
||||
\array_merge(['id' => $uid], $returnPathParams)
|
||||
));
|
||||
}
|
||||
|
||||
foreach($this->validator->validate($user) as $error) {
|
||||
foreach ($this->validator->validate($user) as $error) {
|
||||
$this->addFlash('error', $error->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -233,7 +235,7 @@ class UserController extends CRUDController
|
||||
'edit_form' => $this->createEditForm($user)->createView(),
|
||||
'add_groupcenter_form' => $this->createAddLinkGroupCenterForm($user, $request)->createView(),
|
||||
'delete_groupcenter_form' => array_map(
|
||||
static fn(Form $form) => $form->createView(),
|
||||
static fn (Form $form) => $form->createView(),
|
||||
iterator_to_array($this->getDeleteLinkGroupCenterByUser($user, $request), true)
|
||||
)
|
||||
]);
|
||||
@@ -244,10 +246,10 @@ class UserController extends CRUDController
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
|
||||
$groupCenterManaged = $em->getRepository('ChillMainBundle:GroupCenter')
|
||||
->findOneBy(array(
|
||||
'center' => $groupCenter->getCenter(),
|
||||
'permissionsGroup' => $groupCenter->getPermissionsGroup()
|
||||
));
|
||||
->findOneBy(array(
|
||||
'center' => $groupCenter->getCenter(),
|
||||
'permissionsGroup' => $groupCenter->getPermissionsGroup()
|
||||
));
|
||||
|
||||
if (!$groupCenterManaged) {
|
||||
$em->persist($groupCenter);
|
||||
@@ -267,8 +269,10 @@ class UserController extends CRUDController
|
||||
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
|
||||
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl('admin_user_delete_groupcenter',
|
||||
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])))
|
||||
->setAction($this->generateUrl(
|
||||
'admin_user_delete_groupcenter',
|
||||
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])
|
||||
))
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, array('label' => 'Delete'))
|
||||
->getForm();
|
||||
@@ -282,8 +286,10 @@ class UserController extends CRUDController
|
||||
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
|
||||
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl('admin_user_add_groupcenter',
|
||||
array_merge($returnPathParams, ['uid' => $user->getId()])))
|
||||
->setAction($this->generateUrl(
|
||||
'admin_user_add_groupcenter',
|
||||
array_merge($returnPathParams, ['uid' => $user->getId()])
|
||||
))
|
||||
->setMethod('POST')
|
||||
->add(self::FORM_GROUP_CENTER_COMPOSED, ComposedGroupCenterType::class)
|
||||
->add('submit', SubmitType::class, array('label' => 'Add a new groupCenter'))
|
||||
@@ -296,4 +302,36 @@ class UserController extends CRUDController
|
||||
yield $groupCenter->getId() => $this->createDeleteLinkGroupCenterForm($user, $groupCenter, $request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a form to edit the user current location.
|
||||
*
|
||||
* @Route("/{_locale}/main/user/current-location/edit", name="chill_main_user_currentlocation_edit")
|
||||
*/
|
||||
public function editCurrentLocationAction(Request $request)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$form = $this->createForm(UserCurrentLocationType::class, $user)
|
||||
->add('submit', SubmitType::class, ['label' => 'Save'])
|
||||
->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$currentLocation = $form->get('currentLocation')->getData();
|
||||
|
||||
$user->setCurrentLocation($currentLocation);
|
||||
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
$this->addFlash('success', $this->get('translator')->trans('Current location successfully updated'));
|
||||
|
||||
return $this->redirect(
|
||||
$request->query->has('returnPath') ? $request->query->get('returnPath') :
|
||||
$this->generateUrl('chill_main_homepage')
|
||||
);
|
||||
}
|
||||
|
||||
return $this->render('@ChillMain/User/edit_current_location.html.twig', [
|
||||
'entity' => $user,
|
||||
'edit_form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -1,19 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
*
|
||||
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -23,12 +23,11 @@ namespace Chill\MainBundle\Entity;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="centers")
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class Center implements HasCenterInterface
|
||||
{
|
||||
@@ -46,9 +45,10 @@ class Center implements HasCenterInterface
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string", length=255)
|
||||
* @Serializer\Groups({"docgen:read"})
|
||||
*/
|
||||
private $name;
|
||||
|
||||
private string $name = '';
|
||||
|
||||
/**
|
||||
* @var Collection
|
||||
*
|
||||
@@ -58,8 +58,8 @@ class Center implements HasCenterInterface
|
||||
* )
|
||||
*/
|
||||
private $groupCenters;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Center constructor.
|
||||
*/
|
||||
@@ -67,7 +67,7 @@ class Center implements HasCenterInterface
|
||||
{
|
||||
$this->groupCenters = new \Doctrine\Common\Collections\ArrayCollection();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
@@ -75,7 +75,7 @@ class Center implements HasCenterInterface
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return $this
|
||||
@@ -85,7 +85,7 @@ class Center implements HasCenterInterface
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
@@ -93,7 +93,7 @@ class Center implements HasCenterInterface
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return ArrayCollection|Collection
|
||||
*/
|
||||
@@ -101,7 +101,7 @@ class Center implements HasCenterInterface
|
||||
{
|
||||
return $this->groupCenters;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param GroupCenter $groupCenter
|
||||
* @return $this
|
||||
@@ -111,7 +111,7 @@ class Center implements HasCenterInterface
|
||||
$this->groupCenters->add($groupCenter);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
@@ -119,7 +119,7 @@ class Center implements HasCenterInterface
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return $this|Center
|
||||
*/
|
||||
|
@@ -6,9 +6,10 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
/**
|
||||
* User
|
||||
@@ -16,7 +17,7 @@ use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="users")
|
||||
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region")
|
||||
* @DiscriminatorMap(typeProperty="type", mapping={
|
||||
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
|
||||
* "user"=User::class
|
||||
* })
|
||||
*/
|
||||
@@ -51,6 +52,7 @@ class User implements AdvancedUserInterface {
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=200)
|
||||
* @Serializer\Groups({"docgen:read"})
|
||||
*/
|
||||
private string $label = '';
|
||||
|
||||
@@ -58,8 +60,9 @@ class User implements AdvancedUserInterface {
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string", length=150, nullable=true)
|
||||
* @Serializer\Groups({"docgen:read"})
|
||||
*/
|
||||
private $email;
|
||||
private ?string $email = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
@@ -123,6 +126,7 @@ class User implements AdvancedUserInterface {
|
||||
/**
|
||||
* @var Center|null
|
||||
* @ORM\ManyToOne(targetEntity=Center::class)
|
||||
* @Serializer\Groups({"docgen:read"})
|
||||
*/
|
||||
private ?Center $mainCenter = null;
|
||||
|
||||
@@ -138,6 +142,12 @@ class User implements AdvancedUserInterface {
|
||||
*/
|
||||
private ?UserJob $userJob = null;
|
||||
|
||||
/**
|
||||
* @var Location|null
|
||||
* @ORM\ManyToOne(targetEntity=Location::class)
|
||||
*/
|
||||
private ?Location $currentLocation = null;
|
||||
|
||||
/**
|
||||
* User constructor.
|
||||
*/
|
||||
@@ -485,4 +495,22 @@ class User implements AdvancedUserInterface {
|
||||
$this->userJob = $userJob;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Location|null
|
||||
*/
|
||||
public function getCurrentLocation(): ?Location
|
||||
{
|
||||
return $this->currentLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Location|null $location
|
||||
* @return User
|
||||
*/
|
||||
public function setCurrentLocation(?Location $currentLocation): User
|
||||
{
|
||||
$this->currentLocation = $currentLocation;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@@ -15,15 +15,15 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
final class LocationFormType extends AbstractType
|
||||
{
|
||||
|
||||
// private TranslatableStringHelper $translatableStringHelper;
|
||||
private TranslatableStringHelper $translatableStringHelper;
|
||||
|
||||
// /**
|
||||
// * @param TranslatableStringHelper $translatableStringHelper
|
||||
// */
|
||||
// public function __construct(TranslatableStringHelper $translatableStringHelper)
|
||||
// {
|
||||
// $this->translatableStringHelper = $translatableStringHelper;
|
||||
// }
|
||||
/**
|
||||
* @param TranslatableStringHelper $translatableStringHelper
|
||||
*/
|
||||
public function __construct(TranslatableStringHelper $translatableStringHelper)
|
||||
{
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
@@ -38,8 +38,7 @@ final class LocationFormType extends AbstractType
|
||||
];
|
||||
},
|
||||
'choice_label' => function (EntityLocationType $entity) {
|
||||
//return $this->translatableStringHelper->localize($entity->getTitle()); //TODO not working. Cannot pass smthg in the constructor
|
||||
return $entity->getTitle()['fr'];
|
||||
return $this->translatableStringHelper->localize($entity->getTitle());
|
||||
},
|
||||
])
|
||||
->add('name', TextType::class)
|
||||
|
42
src/Bundle/ChillMainBundle/Form/UserCurrentLocationType.php
Normal file
42
src/Bundle/ChillMainBundle/Form/UserCurrentLocationType.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
use Chill\MainBundle\Repository\LocationRepository;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
|
||||
class UserCurrentLocationType extends AbstractType
|
||||
{
|
||||
private LocationRepository $locationRepository;
|
||||
private TranslatableStringHelper $translatableStringHelper;
|
||||
|
||||
/**
|
||||
* @param TranslatableStringHelper $translatableStringHelper
|
||||
*/
|
||||
public function __construct(TranslatableStringHelper $translatableStringHelper, LocationRepository $locationRepository)
|
||||
{
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
$this->locationRepository = $locationRepository;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('currentLocation', EntityType::class, [
|
||||
'class' => Location::class,
|
||||
'choices' => $this->locationRepository->findByPublicLocations(),
|
||||
'choice_label' => function (Location $entity) {
|
||||
return $entity->getName() ?
|
||||
$entity->getName().' ('.$this->translatableStringHelper->localize($entity->getLocationType()->getTitle()).')' :
|
||||
$this->translatableStringHelper->localize($entity->getLocationType()->getTitle());
|
||||
},
|
||||
'placeholder' => 'Pick a location',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -18,4 +18,14 @@ class LocationRepository extends ServiceEntityRepository
|
||||
{
|
||||
parent::__construct($registry, Location::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch locations which are selectable for users
|
||||
*
|
||||
* @return Location[]
|
||||
*/
|
||||
public function findByPublicLocations()
|
||||
{
|
||||
return $this->findBy(['active' => true, 'availableForUsers' => true]);
|
||||
}
|
||||
}
|
||||
|
@@ -402,3 +402,11 @@ input.belgian_national_number {
|
||||
&.daily_counter {}
|
||||
&.control_digit {}
|
||||
}
|
||||
|
||||
// replace abbr
|
||||
span.item-key {
|
||||
font-variant: all-small-caps;
|
||||
font-size: 90%;
|
||||
background-color: #0000000a;
|
||||
//text-decoration: dotted underline;
|
||||
}
|
||||
|
@@ -8,3 +8,43 @@ require('./bootstrap.scss');
|
||||
import Dropdown from 'bootstrap/js/src/dropdown';
|
||||
import Modal from 'bootstrap/js/dist/modal';
|
||||
import Collapse from 'bootstrap/js/src/collapse';
|
||||
import Carousel from 'bootstrap/js/src/carousel';
|
||||
|
||||
//
|
||||
// ACHeaderSlider is a small slider used in banner of AccompanyingCourse Section
|
||||
// Initialize options, and show/hide controls in first/last slides
|
||||
//
|
||||
let ACHeaderSlider = document.querySelector('#ACHeaderSlider');
|
||||
if (ACHeaderSlider) {
|
||||
let controlPrev = ACHeaderSlider.querySelector('button[data-bs-slide="prev"]'),
|
||||
controlNext = ACHeaderSlider.querySelector('button[data-bs-slide="next"]'),
|
||||
length = ACHeaderSlider.querySelectorAll('.carousel-item').length,
|
||||
last = length-1,
|
||||
carousel = new Carousel(ACHeaderSlider, {
|
||||
interval: false,
|
||||
wrap: false,
|
||||
ride: false,
|
||||
keyboard: false,
|
||||
touch: true
|
||||
})
|
||||
;
|
||||
document.addEventListener('DOMContentLoaded', (e) => {
|
||||
controlNext.classList.remove('visually-hidden');
|
||||
});
|
||||
ACHeaderSlider.addEventListener('slid.bs.carousel', (e) => {
|
||||
//console.log('from slide', e.direction, e.relatedTarget, e.from, e.to );
|
||||
switch (e.to) {
|
||||
case 0:
|
||||
controlPrev.classList.add('visually-hidden');
|
||||
controlNext.classList.remove('visually-hidden');
|
||||
break;
|
||||
case last:
|
||||
controlPrev.classList.remove('visually-hidden');
|
||||
controlNext.classList.add('visually-hidden');
|
||||
break;
|
||||
default:
|
||||
controlPrev.classList.remove('visually-hidden');
|
||||
controlNext.classList.remove('visually-hidden');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -0,0 +1,21 @@
|
||||
{% extends 'ChillMainBundle::layout.html.twig' %}
|
||||
|
||||
{% block title %}{{ 'Edit my current location'|trans }}{% endblock %}
|
||||
|
||||
{% block content -%}
|
||||
<div class="col-md-10 col-xxl">
|
||||
<h1>{{ 'Edit my current location'|trans }}</h1>
|
||||
|
||||
{{ form_start(edit_form) }}
|
||||
{{ form_row(edit_form.currentLocation) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li>
|
||||
{{ form_widget(edit_form.submit, { 'attr': { 'class': 'btn btn-edit' } } ) }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(edit_form) }}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
@@ -5,7 +5,7 @@
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>{{ installation.name }} - {% block title %}{% endblock %}</title>
|
||||
<title>{{ installation.name }} - {% block title %}{{ 'Homepage'|trans }}{% endblock %}</title>
|
||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||
|
||||
{{ encore_entry_link_tags('mod_bootstrap') }}
|
||||
@@ -68,6 +68,9 @@
|
||||
<button type="submit" class="btn btn-lg btn-warning mt-3">
|
||||
<i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }}
|
||||
</button>
|
||||
<a class="btn btn-lg btn-misc mt-3" href="{{ path('chill_main_advanced_search_list') }}">
|
||||
<i class="fa fa-fw fa-search"></i> {{ 'Advanced search'|trans }}
|
||||
</a>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -20,46 +20,57 @@ namespace Chill\MainBundle\Routing\MenuBuilder;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $tokenStorage;
|
||||
|
||||
public function __construct(TokenStorageInterface $tokenStorage)
|
||||
private Security $security;
|
||||
|
||||
public function __construct(Security $security)
|
||||
{
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
|
||||
public function buildMenu($menuId, \Knp\Menu\MenuItem $menu, array $parameters)
|
||||
{
|
||||
if ($this->tokenStorage->getToken()->getUser() instanceof User) {
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if ($user instanceof User) {
|
||||
|
||||
if (null !== $user->getCurrentLocation()) {
|
||||
$locationTextMenu = $user->getCurrentLocation()->getName();
|
||||
} else {
|
||||
$locationTextMenu = 'Set a location';
|
||||
}
|
||||
|
||||
$menu
|
||||
->addChild(
|
||||
$locationTextMenu,
|
||||
['route' => 'chill_main_user_currentlocation_edit']
|
||||
)
|
||||
->setExtras([
|
||||
'order' => -9999999,
|
||||
'icon' => 'map-marker'
|
||||
]);
|
||||
$menu
|
||||
->addChild(
|
||||
'Change password',
|
||||
[ 'route' => 'change_my_password']
|
||||
['route' => 'change_my_password']
|
||||
)
|
||||
->setExtras([
|
||||
'order' => 99999999998
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
$menu
|
||||
->addChild(
|
||||
'Logout',
|
||||
'Logout',
|
||||
[
|
||||
'route' => 'logout'
|
||||
])
|
||||
]
|
||||
)
|
||||
->setExtras([
|
||||
'order'=> 99999999999,
|
||||
'order' => 99999999999,
|
||||
'icon' => 'power-off'
|
||||
]);
|
||||
}
|
||||
|
@@ -26,10 +26,10 @@ use Chill\MainBundle\Search\ParsingException;
|
||||
|
||||
/**
|
||||
* This class implements abstract search with most common responses.
|
||||
*
|
||||
*
|
||||
* you should use this abstract class instead of SearchInterface : if the signature of
|
||||
* search interface change, the generic method will be implemented here.
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*
|
||||
*/
|
||||
@@ -37,7 +37,7 @@ abstract class AbstractSearch implements SearchInterface
|
||||
{
|
||||
/**
|
||||
* parse string expected to be a date and transform to a DateTime object
|
||||
*
|
||||
*
|
||||
* @param type $string
|
||||
* @return \DateTime
|
||||
* @throws ParsingException if the date is not parseable
|
||||
@@ -51,14 +51,14 @@ abstract class AbstractSearch implements SearchInterface
|
||||
. 'not parsable', 0, $ex);
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* recompose a pattern, retaining only supported terms
|
||||
*
|
||||
*
|
||||
* the outputted string should be used to show users their search
|
||||
*
|
||||
*
|
||||
* @param array $terms
|
||||
* @param array $supportedTerms
|
||||
* @param string $domain if your domain is NULL, you should set NULL. You should set used domain instead
|
||||
@@ -67,35 +67,35 @@ abstract class AbstractSearch implements SearchInterface
|
||||
protected function recomposePattern(array $terms, array $supportedTerms, $domain = NULL)
|
||||
{
|
||||
$recomposed = '';
|
||||
|
||||
|
||||
if ($domain !== NULL)
|
||||
{
|
||||
$recomposed .= '@'.$domain.' ';
|
||||
}
|
||||
|
||||
|
||||
foreach ($supportedTerms as $term) {
|
||||
if (array_key_exists($term, $terms) && $term !== '_default') {
|
||||
$recomposed .= ' '.$term.':';
|
||||
$containsSpace = \strpos($terms[$term], " ") !== false;
|
||||
if ($containsSpace) {
|
||||
$recomposed .= "(";
|
||||
$recomposed .= '"';
|
||||
}
|
||||
$recomposed .= (mb_stristr(' ', $terms[$term]) === FALSE) ? $terms[$term] : '('.$terms[$term].')';
|
||||
if ($containsSpace) {
|
||||
$recomposed .= ")";
|
||||
$recomposed .= '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($terms['_default'] !== '') {
|
||||
$recomposed .= ' '.$terms['_default'];
|
||||
}
|
||||
|
||||
|
||||
//strip first character if empty
|
||||
if (mb_strcut($recomposed, 0, 1) === ' '){
|
||||
$recomposed = mb_strcut($recomposed, 1);
|
||||
}
|
||||
|
||||
|
||||
return $recomposed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,19 +20,15 @@ class SearchApi
|
||||
private EntityManagerInterface $em;
|
||||
private PaginatorFactory $paginator;
|
||||
|
||||
private array $providers = [];
|
||||
private iterable $providers = [];
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
SearchPersonApiProvider $searchPerson,
|
||||
ThirdPartyApiSearch $thirdPartyApiSearch,
|
||||
SearchUserApiProvider $searchUser,
|
||||
iterable $providers,
|
||||
PaginatorFactory $paginator
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->providers[] = $searchPerson;
|
||||
$this->providers[] = $thirdPartyApiSearch;
|
||||
$this->providers[] = $searchUser;
|
||||
$this->providers = $providers;
|
||||
$this->paginator = $paginator;
|
||||
}
|
||||
|
||||
@@ -68,10 +64,15 @@ class SearchApi
|
||||
|
||||
private function findProviders(string $pattern, array $types, array $parameters): array
|
||||
{
|
||||
return \array_filter(
|
||||
$this->providers,
|
||||
fn($p) => $p->supportsTypes($pattern, $types, $parameters)
|
||||
);
|
||||
$providers = [];
|
||||
|
||||
foreach ($this->providers as $provider) {
|
||||
if ($provider->supportsTypes($pattern, $types, $parameters)) {
|
||||
$providers[] = $provider;
|
||||
}
|
||||
}
|
||||
|
||||
return $providers;
|
||||
}
|
||||
|
||||
private function countItems($providers, $types, $parameters): int
|
||||
@@ -82,12 +83,12 @@ class SearchApi
|
||||
$countNq = $this->em->createNativeQuery($countQuery, $rsmCount);
|
||||
$countNq->setParameters($parameters);
|
||||
|
||||
return $countNq->getSingleScalarResult();
|
||||
return (int) $countNq->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function buildCountQuery(array $queries, $types, $parameters)
|
||||
{
|
||||
$query = "SELECT COUNT(*) AS count FROM ({union_unordered}) AS sq";
|
||||
$query = "SELECT SUM(c) AS count FROM ({union_unordered}) AS sq";
|
||||
$unions = [];
|
||||
$parameters = [];
|
||||
|
||||
@@ -141,17 +142,20 @@ class SearchApi
|
||||
private function prepareProviders(array $rawResults)
|
||||
{
|
||||
$metadatas = [];
|
||||
$providers = [];
|
||||
|
||||
foreach ($rawResults as $r) {
|
||||
foreach ($this->providers as $k => $p) {
|
||||
if ($p->supportsResult($r['key'], $r['metadata'])) {
|
||||
$metadatas[$k][] = $r['metadata'];
|
||||
$providers[$k] = $p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($metadatas as $k => $m) {
|
||||
$this->providers[$k]->prepare($m);
|
||||
$providers[$k]->prepare($m);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,8 @@ namespace Chill\MainBundle\Search;
|
||||
|
||||
class SearchApiQuery
|
||||
{
|
||||
private array $select = [];
|
||||
private array $selectParams = [];
|
||||
private ?string $selectKey = null;
|
||||
private array $selectKeyParams = [];
|
||||
private ?string $jsonbMetadata = null;
|
||||
@@ -15,6 +17,38 @@ class SearchApiQuery
|
||||
private array $whereClauses = [];
|
||||
private array $whereClausesParams = [];
|
||||
|
||||
public function addSelectClause(string $select, array $params = []): self
|
||||
{
|
||||
$this->select[] = $select;
|
||||
$this->selectParams = [...$this->selectParams, ...$params];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function resetSelectClause(): self
|
||||
{
|
||||
$this->select = [];
|
||||
$this->selectParams = [];
|
||||
$this->selectKey = null;
|
||||
$this->selectKeyParams = [];
|
||||
$this->jsonbMetadata = null;
|
||||
$this->jsonbMetadataParams = [];
|
||||
$this->pertinence = null;
|
||||
$this->pertinenceParams = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSelectClauses(): array
|
||||
{
|
||||
return $this->select;
|
||||
}
|
||||
|
||||
public function getSelectParams(): array
|
||||
{
|
||||
return $this->selectParams;
|
||||
}
|
||||
|
||||
public function setSelectKey(string $selectKey, array $params = []): self
|
||||
{
|
||||
$this->selectKey = $selectKey;
|
||||
@@ -47,6 +81,16 @@ class SearchApiQuery
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFromClause(): string
|
||||
{
|
||||
return $this->fromClause;
|
||||
}
|
||||
|
||||
public function getFromParams(): array
|
||||
{
|
||||
return $this->fromClauseParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the where clause and replace all existing ones.
|
||||
*
|
||||
@@ -54,7 +98,7 @@ class SearchApiQuery
|
||||
public function setWhereClauses(string $whereClause, array $params = []): self
|
||||
{
|
||||
$this->whereClauses = [$whereClause];
|
||||
$this->whereClausesParams = [$params];
|
||||
$this->whereClausesParams = $params;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -71,11 +115,53 @@ class SearchApiQuery
|
||||
public function andWhereClause(string $whereClause, array $params = []): self
|
||||
{
|
||||
$this->whereClauses[] = $whereClause;
|
||||
$this->whereClausesParams[] = $params;
|
||||
\array_push($this->whereClausesParams, ...$params);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function buildSelectParams(bool $count = false): array
|
||||
{
|
||||
if ($count) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$args = $this->getSelectParams();
|
||||
|
||||
if (null !== $this->selectKey) {
|
||||
$args = [...$args, ...$this->selectKeyParams];
|
||||
}
|
||||
if (null !== $this->jsonbMetadata) {
|
||||
$args = [...$args, ...$this->jsonbMetadataParams];
|
||||
}
|
||||
if (null !== $this->pertinence) {
|
||||
$args = [...$args, ...$this->pertinenceParams];
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
private function buildSelectClause(bool $countOnly = false): string
|
||||
{
|
||||
if ($countOnly) {
|
||||
return 'count(*) AS c';
|
||||
}
|
||||
|
||||
$selects = $this->getSelectClauses();
|
||||
|
||||
if (null !== $this->selectKey) {
|
||||
$selects[] = \strtr("'{key}' AS key", [ '{key}' => $this->selectKey ]);
|
||||
}
|
||||
if (null !== $this->jsonbMetadata) {
|
||||
$selects[] = \strtr('{metadata} AS metadata', [ '{metadata}' => $this->jsonbMetadata]);
|
||||
}
|
||||
if (null !== $this->pertinence) {
|
||||
$selects[] = \strtr('{pertinence} AS pertinence', [ '{pertinence}' => $this->pertinence]);
|
||||
}
|
||||
|
||||
return \implode(', ', $selects);
|
||||
}
|
||||
|
||||
public function buildQuery(bool $countOnly = false): string
|
||||
{
|
||||
$isMultiple = count($this->whereClauses);
|
||||
@@ -87,19 +173,8 @@ class SearchApiQuery
|
||||
($isMultiple ? ')' : '')
|
||||
;
|
||||
|
||||
if (!$countOnly) {
|
||||
$select = \strtr("
|
||||
'{key}' AS key,
|
||||
{metadata} AS metadata,
|
||||
{pertinence} AS pertinence
|
||||
", [
|
||||
'{key}' => $this->selectKey,
|
||||
'{metadata}' => $this->jsonbMetadata,
|
||||
'{pertinence}' => $this->pertinence,
|
||||
]);
|
||||
} else {
|
||||
$select = "1 AS c";
|
||||
}
|
||||
$select = $this->buildSelectClause($countOnly);
|
||||
|
||||
|
||||
return \strtr("SELECT
|
||||
{select}
|
||||
@@ -116,18 +191,16 @@ class SearchApiQuery
|
||||
public function buildParameters(bool $countOnly = false): array
|
||||
{
|
||||
if (!$countOnly) {
|
||||
return \array_merge(
|
||||
$this->selectKeyParams,
|
||||
$this->jsonbMetadataParams,
|
||||
$this->pertinenceParams,
|
||||
$this->fromClauseParams,
|
||||
\array_merge([], ...$this->whereClausesParams),
|
||||
);
|
||||
return [
|
||||
...$this->buildSelectParams($countOnly),
|
||||
...$this->fromClauseParams,
|
||||
...$this->whereClausesParams,
|
||||
];
|
||||
} else {
|
||||
return \array_merge(
|
||||
$this->fromClauseParams,
|
||||
\array_merge([], ...$this->whereClausesParams),
|
||||
);
|
||||
return [
|
||||
...$this->fromClauseParams,
|
||||
...$this->whereClausesParams,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,10 +10,10 @@ use Chill\MainBundle\Search\HasAdvancedSearchFormInterface;
|
||||
* installed into the app.
|
||||
* the service is callable from the container with
|
||||
* $container->get('chill_main.search_provider')
|
||||
*
|
||||
* the syntax for search string is :
|
||||
* - domain, which begin with `@`. Example: `@person`. Restrict the search to some
|
||||
* entities. It may exists multiple search provider for the same domain (example:
|
||||
*
|
||||
* the syntax for search string is :
|
||||
* - domain, which begin with `@`. Example: `@person`. Restrict the search to some
|
||||
* entities. It may exists multiple search provider for the same domain (example:
|
||||
* a search provider for people which performs regular search, and suggestion search
|
||||
* with phonetical algorithms
|
||||
* - terms, which are the terms of the search. There are terms with argument (example :
|
||||
@@ -25,17 +25,17 @@ class SearchProvider
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @var SearchInterface[]
|
||||
*/
|
||||
private $searchServices = array();
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @var HasAdvancedSearchForm[]
|
||||
*/
|
||||
private $hasAdvancedFormSearchServices = array();
|
||||
|
||||
|
||||
/*
|
||||
* return search services in an array, ordered by
|
||||
* the order key (defined in service definition)
|
||||
@@ -59,7 +59,7 @@ class SearchProvider
|
||||
|
||||
return $this->searchServices;
|
||||
}
|
||||
|
||||
|
||||
public function getHasAdvancedFormSearchServices()
|
||||
{
|
||||
//sort the array
|
||||
@@ -75,7 +75,7 @@ class SearchProvider
|
||||
|
||||
/**
|
||||
* parse the search string to extract domain and terms
|
||||
*
|
||||
*
|
||||
* @param string $pattern
|
||||
* @return string[] an array where the keys are _domain, _default (residual terms) or term
|
||||
*/
|
||||
@@ -95,9 +95,9 @@ class SearchProvider
|
||||
|
||||
/**
|
||||
* Extract the domain of the subject
|
||||
*
|
||||
*
|
||||
* The domain begins with `@`. Example: `@person`, `@report`, ....
|
||||
*
|
||||
*
|
||||
* @param type $subject
|
||||
* @return string
|
||||
* @throws ParsingException
|
||||
@@ -121,14 +121,15 @@ class SearchProvider
|
||||
private function extractTerms(&$subject)
|
||||
{
|
||||
$terms = array();
|
||||
preg_match_all('/([a-z\-]+):([\w\-]+|\([^\(\r\n]+\))/', $subject, $matches);
|
||||
$matches = [];
|
||||
preg_match_all('/([a-z\-]+):(([^"][\S\-]+)|"[^"]*")/', $subject, $matches);
|
||||
|
||||
foreach ($matches[2] as $key => $match) {
|
||||
//remove from search pattern
|
||||
$this->mustBeExtracted[] = $matches[0][$key];
|
||||
//strip parenthesis
|
||||
if (mb_substr($match, 0, 1) === '(' &&
|
||||
mb_substr($match, mb_strlen($match) - 1) === ')') {
|
||||
if (mb_substr($match, 0, 1) === '"' &&
|
||||
mb_substr($match, mb_strlen($match) - 1) === '"') {
|
||||
$match = trim(mb_substr($match, 1, mb_strlen($match) - 2));
|
||||
}
|
||||
$terms[$matches[1][$key]] = $match;
|
||||
@@ -139,14 +140,14 @@ class SearchProvider
|
||||
|
||||
/**
|
||||
* store string which must be extracted to find default arguments
|
||||
*
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $mustBeExtracted = array();
|
||||
|
||||
/**
|
||||
* extract default (residual) arguments
|
||||
*
|
||||
*
|
||||
* @param string $subject
|
||||
* @return string
|
||||
*/
|
||||
@@ -158,7 +159,7 @@ class SearchProvider
|
||||
/**
|
||||
* search through services which supports domain and give
|
||||
* results as an array of resultsfrom different SearchInterface
|
||||
*
|
||||
*
|
||||
* @param string $pattern
|
||||
* @param number $start
|
||||
* @param number $limit
|
||||
@@ -167,25 +168,25 @@ class SearchProvider
|
||||
* @return array of results from different SearchInterface
|
||||
* @throws UnknowSearchDomainException if the domain is unknow
|
||||
*/
|
||||
public function getSearchResults($pattern, $start = 0, $limit = 50,
|
||||
public function getSearchResults($pattern, $start = 0, $limit = 50,
|
||||
array $options = array(), $format = 'html')
|
||||
{
|
||||
$terms = $this->parse($pattern);
|
||||
$results = array();
|
||||
|
||||
|
||||
//sort searchServices by order
|
||||
$sortedSearchServices = array();
|
||||
foreach($this->searchServices as $service) {
|
||||
$sortedSearchServices[$service->getOrder()] = $service;
|
||||
}
|
||||
|
||||
|
||||
if ($terms['_domain'] !== NULL) {
|
||||
foreach ($sortedSearchServices as $service) {
|
||||
if ($service->supports($terms['_domain'], $format)) {
|
||||
$results[] = $service->renderResult($terms, $start, $limit, $options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (count($results) === 0) {
|
||||
throw new UnknowSearchDomainException($terms['_domain']);
|
||||
}
|
||||
@@ -196,24 +197,24 @@ class SearchProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//sort array
|
||||
ksort($results);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
public function getResultByName($pattern, $name, $start = 0, $limit = 50,
|
||||
array $options = array(), $format = 'html')
|
||||
array $options = array(), $format = 'html')
|
||||
{
|
||||
$terms = $this->parse($pattern);
|
||||
$search = $this->getByName($name);
|
||||
|
||||
|
||||
if ($terms['_domain'] !== NULL && !$search->supports($terms['_domain'], $format))
|
||||
{
|
||||
throw new ParsingException("The domain is not supported for the search name");
|
||||
}
|
||||
|
||||
|
||||
return $search->renderResult($terms, $start, $limit, $options, $format);
|
||||
}
|
||||
|
||||
@@ -232,16 +233,16 @@ class SearchProvider
|
||||
throw new UnknowSearchNameException($name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* return searchservice with an advanced form, defined in service
|
||||
* return searchservice with an advanced form, defined in service
|
||||
* definition.
|
||||
*
|
||||
*
|
||||
* @param string $name
|
||||
* @return HasAdvancedSearchForm
|
||||
* @throws UnknowSearchNameException
|
||||
*/
|
||||
public function getHasAdvancedFormByName($name)
|
||||
public function getHasAdvancedFormByName($name)
|
||||
{
|
||||
if (\array_key_exists($name, $this->hasAdvancedFormSearchServices)) {
|
||||
return $this->hasAdvancedFormSearchServices[$name];
|
||||
@@ -253,7 +254,7 @@ class SearchProvider
|
||||
public function addSearchService(SearchInterface $service, $name)
|
||||
{
|
||||
$this->searchServices[$name] = $service;
|
||||
|
||||
|
||||
if ($service instanceof HasAdvancedSearchFormInterface) {
|
||||
$this->hasAdvancedFormSearchServices[$name] = $service;
|
||||
}
|
||||
@@ -477,7 +478,7 @@ class SearchProvider
|
||||
$string = strtr($string, $chars);
|
||||
} /* remove from wordpress: we use only utf 8
|
||||
* else {
|
||||
|
||||
|
||||
// Assume ISO-8859-1 if not UTF-8
|
||||
$chars['in'] = chr(128) . chr(131) . chr(138) . chr(142) . chr(154) . chr(158)
|
||||
. chr(159) . chr(162) . chr(165) . chr(181) . chr(192) . chr(193) . chr(194)
|
||||
|
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Search\Utils;
|
||||
|
||||
use \DateTimeImmutable;
|
||||
|
||||
class ExtractDateFromPattern
|
||||
{
|
||||
private const DATE_PATTERN = [
|
||||
["([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))", 'Y-m-d'], // 1981-05-12
|
||||
["((0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([12]\d{3}))", 'd/m/Y'], // 15/12/1980
|
||||
["((0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])-([12]\d{3}))", 'd-m-Y'], // 15/12/1980
|
||||
];
|
||||
|
||||
public function extractDates(string $subject): SearchExtractionResult
|
||||
{
|
||||
$dates = [];
|
||||
$filteredSubject = $subject;
|
||||
|
||||
foreach (self::DATE_PATTERN as [$pattern, $format]) {
|
||||
$matches = [];
|
||||
\preg_match_all($pattern, $filteredSubject, $matches);
|
||||
|
||||
foreach ($matches[0] as $match) {
|
||||
$date = DateTimeImmutable::createFromFormat($format, $match);
|
||||
if (false !== $date) {
|
||||
$dates[] = $date;
|
||||
// filter string: remove what is found
|
||||
$filteredSubject = \trim(\strtr($filteredSubject, [$match => ""]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SearchExtractionResult($filteredSubject, $dates);
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Search\Utils;
|
||||
|
||||
|
||||
class ExtractPhonenumberFromPattern
|
||||
{
|
||||
private const PATTERN = "([\+]{0,1}[0-9\ ]{5,})";
|
||||
|
||||
public function extractPhonenumber(string $subject): SearchExtractionResult
|
||||
{
|
||||
$matches = [];
|
||||
\preg_match(self::PATTERN, $subject,$matches);
|
||||
|
||||
if (0 < count($matches)) {
|
||||
$phonenumber = [];
|
||||
$length = 0;
|
||||
|
||||
foreach (\str_split(\trim($matches[0])) as $key => $char) {
|
||||
switch ($char) {
|
||||
case '0':
|
||||
$length++;
|
||||
if ($key === 0) { $phonenumber[] = '+32'; }
|
||||
else { $phonenumber[] = $char; }
|
||||
break;
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
$length++;
|
||||
$phonenumber[] = $char;
|
||||
break;
|
||||
case ' ':
|
||||
break;
|
||||
default:
|
||||
throw new \LogicException("should not match not alnum character");
|
||||
}
|
||||
}
|
||||
|
||||
if ($length > 5) {
|
||||
$filtered = \trim(\strtr($subject, [$matches[0] => '']));
|
||||
|
||||
return new SearchExtractionResult($filtered, [\implode('', $phonenumber)] );
|
||||
}
|
||||
}
|
||||
|
||||
return new SearchExtractionResult($subject, []);
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Search\Utils;
|
||||
|
||||
class SearchExtractionResult
|
||||
{
|
||||
private string $filteredSubject;
|
||||
private array $found;
|
||||
|
||||
public function __construct(string $filteredSubject, array $found)
|
||||
{
|
||||
$this->filteredSubject = $filteredSubject;
|
||||
$this->found = $found;
|
||||
}
|
||||
|
||||
public function getFound(): array
|
||||
{
|
||||
return $this->found;
|
||||
}
|
||||
|
||||
public function hasResult(): bool
|
||||
{
|
||||
return [] !== $this->found;
|
||||
}
|
||||
|
||||
public function getFilteredSubject(): string
|
||||
{
|
||||
return $this->filteredSubject;
|
||||
}
|
||||
}
|
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Security\Resolver;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
|
||||
final class CenterResolverManager implements CenterResolverManagerInterface
|
||||
{
|
||||
/**
|
||||
@@ -20,7 +22,17 @@ final class CenterResolverManager implements CenterResolverManagerInterface
|
||||
{
|
||||
foreach($this->resolvers as $resolver) {
|
||||
if ($resolver->supports($entity, $options)) {
|
||||
return (array) $resolver->resolveCenter($entity, $options);
|
||||
$resolved = $resolver->resolveCenter($entity, $options);
|
||||
if (null === $resolved) {
|
||||
return [];
|
||||
} elseif ($resolved instanceof Center) {
|
||||
return [$resolved];
|
||||
} elseif (\is_array($resolved)) {
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
throw new \UnexpectedValueException(sprintf("the return type of a %s should be an instance of %s, an array or null. Resolver is %s",
|
||||
CenterResolverInterface::class, Center::class, get_class($resolver)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,20 +6,21 @@ use Twig\TwigFilter;
|
||||
|
||||
final class ResolverTwigExtension extends \Twig\Extension\AbstractExtension
|
||||
{
|
||||
private CenterResolverDispatcher $centerResolverDispatcher;
|
||||
private CenterResolverManagerInterface $centerResolverDispatcher;
|
||||
private ScopeResolverDispatcher $scopeResolverDispatcher;
|
||||
|
||||
/**
|
||||
* @param CenterResolverDispatcher $centerResolverDispatcher
|
||||
*/
|
||||
public function __construct(CenterResolverDispatcher $centerResolverDispatcher)
|
||||
public function __construct(CenterResolverManagerInterface $centerResolverDispatcher, ScopeResolverDispatcher $scopeResolverDispatcher)
|
||||
{
|
||||
$this->centerResolverDispatcher = $centerResolverDispatcher;
|
||||
$this->scopeResolverDispatcher = $scopeResolverDispatcher;
|
||||
}
|
||||
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new TwigFilter('chill_resolve_center', [$this, 'resolveCenter'])
|
||||
new TwigFilter('chill_resolve_center', [$this, 'resolveCenter']),
|
||||
new TwigFilter('chill_resolve_scope', [$this, 'resolveScope']),
|
||||
new TwigFilter('chill_is_scope_concerned', [$this, 'isScopeConcerned']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -30,7 +31,26 @@ final class ResolverTwigExtension extends \Twig\Extension\AbstractExtension
|
||||
*/
|
||||
public function resolveCenter($entity, ?array $options = [])
|
||||
{
|
||||
return $this->centerResolverDispatcher->resolveCenter($entity, $options);
|
||||
return $this->centerResolverDispatcher->resolveCenters($entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $entity
|
||||
* @param array|null $options
|
||||
* @return bool
|
||||
*/
|
||||
public function isScopeConcerned($entity, ?array $options = [])
|
||||
{
|
||||
return $this->scopeResolverDispatcher->isConcerned($entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $entity
|
||||
* @param array|null $options
|
||||
* @return array|\Chill\MainBundle\Entity\Scope|\Chill\MainBundle\Entity\Scope[]
|
||||
*/
|
||||
public function resolveScope($entity, ?array $options = [])
|
||||
{
|
||||
return $this->scopeResolverDispatcher->resolveScope();
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +1,18 @@
|
||||
<?php
|
||||
/*
|
||||
*
|
||||
*
|
||||
* Copyright (C) 2014-2021, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -27,7 +27,7 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
class CenterNormalizer implements NormalizerInterface, DenormalizerInterface
|
||||
@@ -52,7 +52,7 @@ class CenterNormalizer implements NormalizerInterface, DenormalizerInterface
|
||||
|
||||
public function supportsNormalization($data, string $format = null): bool
|
||||
{
|
||||
return $data instanceof Center;
|
||||
return $data instanceof Center && $format === 'json';
|
||||
}
|
||||
|
||||
public function denormalize($data, string $type, string $format = null, array $context = [])
|
||||
|
@@ -19,23 +19,78 @@
|
||||
|
||||
namespace Chill\MainBundle\Serializer\Normalizer;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
|
||||
class DateNormalizer implements NormalizerInterface, DenormalizerInterface
|
||||
class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInterface
|
||||
{
|
||||
private RequestStack $requestStack;
|
||||
private ParameterBagInterface $parameterBag;
|
||||
|
||||
public function __construct(RequestStack $requestStack, ParameterBagInterface $parameterBag)
|
||||
{
|
||||
$this->requestStack = $requestStack;
|
||||
$this->parameterBag = $parameterBag;
|
||||
}
|
||||
|
||||
public function normalize($date, string $format = null, array $context = array())
|
||||
{
|
||||
/** @var \DateTimeInterface $date */
|
||||
return [
|
||||
'datetime' => $date->format(\DateTimeInterface::ISO8601)
|
||||
];
|
||||
switch($format) {
|
||||
case 'json':
|
||||
return [
|
||||
'datetime' => $date->format(\DateTimeInterface::ISO8601)
|
||||
];
|
||||
case 'docgen':
|
||||
|
||||
if (null === $date) {
|
||||
return [
|
||||
'long' => '', 'short' => ''
|
||||
];
|
||||
}
|
||||
|
||||
$hasTime = $date->format('His') !== "000000";
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
$locale = null !== $request ? $request->getLocale() : $this->parameterBag->get('kernel.default_locale');
|
||||
$formatterLong = \IntlDateFormatter::create(
|
||||
$locale,
|
||||
\IntlDateFormatter::LONG,
|
||||
$hasTime ? \IntlDateFormatter::SHORT: \IntlDateFormatter::NONE
|
||||
);
|
||||
$formatterShort = \IntlDateFormatter::create(
|
||||
$locale,
|
||||
\IntlDateFormatter::SHORT,
|
||||
$hasTime ? \IntlDateFormatter::SHORT: \IntlDateFormatter::NONE
|
||||
);
|
||||
|
||||
return [
|
||||
'short' => $formatterShort->format($date),
|
||||
'long' => $formatterLong->format($date)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, string $format = null): bool
|
||||
public function supportsNormalization($data, string $format = null, array $context = []): bool
|
||||
{
|
||||
return $data instanceof \DateTimeInterface;
|
||||
if ($format === 'json') {
|
||||
return $data instanceof \DateTimeInterface;
|
||||
} elseif ($format === 'docgen') {
|
||||
return $data instanceof \DateTimeInterface || (
|
||||
$data === null
|
||||
&& \array_key_exists('docgen:expects', $context)
|
||||
&& (
|
||||
$context['docgen:expects'] === \DateTimeInterface::class
|
||||
|| $context['docgen:expects'] === \DateTime::class
|
||||
|| $context['docgen:expects'] === \DateTimeImmutable::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function denormalize($data, string $type, string $format = null, array $context = [])
|
||||
|
@@ -52,6 +52,6 @@ class UserNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||
|
||||
public function supportsNormalization($data, string $format = null): bool
|
||||
{
|
||||
return $data instanceof User;
|
||||
return $format === 'json' && $data instanceof User;
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ class SearchApiQueryTest extends TestCase
|
||||
$q = new SearchApiQuery();
|
||||
$q->setSelectJsonbMetadata('boum')
|
||||
->setSelectKey('bim')
|
||||
->setSelectPertinence('1')
|
||||
->setSelectPertinence('?', ['gamma'])
|
||||
->setFromClause('badaboum')
|
||||
->andWhereClause('foo', [ 'alpha' ])
|
||||
->andWhereClause('bar', [ 'beta' ])
|
||||
@@ -21,12 +21,12 @@ class SearchApiQueryTest extends TestCase
|
||||
$query = $q->buildQuery();
|
||||
|
||||
$this->assertStringContainsString('(foo) AND (bar)', $query);
|
||||
$this->assertEquals(['alpha', 'beta'], $q->buildParameters());
|
||||
$this->assertEquals(['gamma', 'alpha', 'beta'], $q->buildParameters());
|
||||
|
||||
$query = $q->buildQuery(true);
|
||||
|
||||
$this->assertStringContainsString('(foo) AND (bar)', $query);
|
||||
$this->assertEquals(['alpha', 'beta'], $q->buildParameters());
|
||||
$this->assertEquals(['gamma', 'alpha', 'beta'], $q->buildParameters());
|
||||
}
|
||||
|
||||
public function testWithoutWhereClause()
|
||||
@@ -42,4 +42,20 @@ class SearchApiQueryTest extends TestCase
|
||||
$this->assertEquals([], $q->buildParameters());
|
||||
}
|
||||
|
||||
public function testBuildParams()
|
||||
{
|
||||
$q = new SearchApiQuery();
|
||||
|
||||
$q
|
||||
->addSelectClause('bada', [ 'one', 'two' ])
|
||||
->addSelectClause('boum', ['three', 'four'])
|
||||
->setWhereClauses('mywhere', [ 'six', 'seven'])
|
||||
;
|
||||
|
||||
$params = $q->buildParameters();
|
||||
|
||||
$this->assertEquals(['six', 'seven'], $q->buildParameters(true));
|
||||
$this->assertEquals(['one', 'two', 'three', 'four', 'six', 'seven'], $q->buildParameters());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -26,17 +26,17 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SearchProviderTest extends TestCase
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @var SearchProvider
|
||||
* @var SearchProvider
|
||||
*/
|
||||
private $search;
|
||||
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->search = new SearchProvider();
|
||||
|
||||
|
||||
//add a default service
|
||||
$this->addSearchService(
|
||||
$this->createDefaultSearchService('I am default', 10), 'default'
|
||||
@@ -46,7 +46,7 @@ class SearchProviderTest extends TestCase
|
||||
$this->createNonDefaultDomainSearchService('I am domain bar', 20, FALSE), 'bar'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @expectedException \Chill\MainBundle\Search\UnknowSearchNameException
|
||||
*/
|
||||
@@ -54,11 +54,11 @@ class SearchProviderTest extends TestCase
|
||||
{
|
||||
$this->search->getByName("invalid name");
|
||||
}
|
||||
|
||||
|
||||
public function testSimplePattern()
|
||||
{
|
||||
$terms = $this->p("@person birthdate:2014-01-02 name:(my name) is not my name");
|
||||
|
||||
$terms = $this->p("@person birthdate:2014-01-02 name:\"my name\" is not my name");
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => 'person',
|
||||
'birthdate' => '2014-01-02',
|
||||
@@ -66,40 +66,40 @@ class SearchProviderTest extends TestCase
|
||||
'name' => 'my name'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testWithoutDomain()
|
||||
{
|
||||
$terms = $this->p('foo:bar residual');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => null,
|
||||
'foo' => 'bar',
|
||||
'_default' => 'residual'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testWithoutDefault()
|
||||
{
|
||||
$terms = $this->p('@person foo:bar');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => 'person',
|
||||
'foo' => 'bar',
|
||||
'_default' => ''
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testCapitalLetters()
|
||||
{
|
||||
$terms = $this->p('Foo:Bar LOL marCi @PERSON');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => 'person',
|
||||
'_default' => 'lol marci',
|
||||
'foo' => 'bar'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @expectedException Chill\MainBundle\Search\ParsingException
|
||||
*/
|
||||
@@ -107,12 +107,11 @@ class SearchProviderTest extends TestCase
|
||||
{
|
||||
$term = $this->p("@person @report");
|
||||
}
|
||||
|
||||
|
||||
public function testDoubleParenthesis()
|
||||
{
|
||||
$terms = $this->p("@papamobile name:(my beautiful name) residual "
|
||||
. "surname:(i love techno)");
|
||||
|
||||
$terms = $this->p('@papamobile name:"my beautiful name" residual surname:"i love techno"');
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => 'papamobile',
|
||||
'name' => 'my beautiful name',
|
||||
@@ -120,65 +119,65 @@ class SearchProviderTest extends TestCase
|
||||
'surname' => 'i love techno'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testAccentued()
|
||||
{
|
||||
//$this->markTestSkipped('accentued characters must be implemented');
|
||||
|
||||
|
||||
$terms = $this->p('manço bélier aztèque à saloù ê');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => NULL,
|
||||
'_default' => 'manco belier azteque a salou e'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testAccentuedCapitals()
|
||||
{
|
||||
//$this->markTestSkipped('accentued characters must be implemented');
|
||||
|
||||
|
||||
$terms = $this->p('MANÉÀ oÛ lÎ À');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => null,
|
||||
'_default' => 'manea ou li a'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testTrimInParenthesis()
|
||||
{
|
||||
$terms = $this->p('foo:(bar )');
|
||||
|
||||
$terms = $this->p('foo:"bar "');
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => null,
|
||||
'foo' => 'bar',
|
||||
'_default' => ''
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testTrimInDefault()
|
||||
{
|
||||
$terms = $this->p(' foo bar ');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => null,
|
||||
'_default' => 'foo bar'
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
public function testArgumentNameWithTrait()
|
||||
{
|
||||
$terms = $this->p('date-from:2016-05-04');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
'_domain' => null,
|
||||
'date-from' => '2016-05-04',
|
||||
'_default' => ''
|
||||
), $terms);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test the behaviour when no domain is provided in the search pattern :
|
||||
* Test the behaviour when no domain is provided in the search pattern :
|
||||
* the default search should be enabled
|
||||
*/
|
||||
public function testSearchResultDefault()
|
||||
@@ -186,12 +185,12 @@ class SearchProviderTest extends TestCase
|
||||
$response = $this->search->getSearchResults('default search');
|
||||
|
||||
//$this->markTestSkipped();
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
"I am default"
|
||||
), $response);
|
||||
), $response);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @expectedException \Chill\MainBundle\Search\UnknowSearchDomainException
|
||||
*/
|
||||
@@ -200,49 +199,49 @@ class SearchProviderTest extends TestCase
|
||||
$response = $this->search->getSearchResults('@unknow domain');
|
||||
|
||||
//$this->markTestSkipped();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function testSearchResultDomainSearch()
|
||||
{
|
||||
//add a search service which will be supported
|
||||
$this->addSearchService(
|
||||
$this->createNonDefaultDomainSearchService("I am domain foo", 100, TRUE), 'foo'
|
||||
);
|
||||
|
||||
|
||||
$response = $this->search->getSearchResults('@foo default search');
|
||||
|
||||
|
||||
$this->assertEquals(array(
|
||||
"I am domain foo"
|
||||
), $response);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function testSearchWithinSpecificSearchName()
|
||||
{
|
||||
//add a search service which will be supported
|
||||
$this->addSearchService(
|
||||
$this->createNonDefaultDomainSearchService("I am domain foo", 100, TRUE), 'foo'
|
||||
);
|
||||
|
||||
|
||||
$response = $this->search->getResultByName('@foo search', 'foo');
|
||||
|
||||
|
||||
$this->assertEquals('I am domain foo', $response);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @expectedException \Chill\MainBundle\Search\ParsingException
|
||||
*/
|
||||
public function testSearchWithinSpecificSearchNameInConflictWithSupport()
|
||||
{
|
||||
$response = $this->search->getResultByName('@foo default search', 'bar');
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* shortcut for executing parse method
|
||||
*
|
||||
*
|
||||
* @param unknown $pattern
|
||||
* @return string[]
|
||||
*/
|
||||
@@ -250,12 +249,12 @@ class SearchProviderTest extends TestCase
|
||||
{
|
||||
return $this->search->parse($pattern);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a search service to the chill.main.search_provider
|
||||
*
|
||||
*
|
||||
* Useful for mocking the SearchInterface
|
||||
*
|
||||
*
|
||||
* @param SearchInterface $search
|
||||
* @param string $name
|
||||
*/
|
||||
@@ -264,52 +263,52 @@ class SearchProviderTest extends TestCase
|
||||
$this->search
|
||||
->addSearchService($search, $name);
|
||||
}
|
||||
|
||||
|
||||
private function createDefaultSearchService($result, $order)
|
||||
{
|
||||
$mock = $this
|
||||
->getMockForAbstractClass('Chill\MainBundle\Search\AbstractSearch');
|
||||
|
||||
|
||||
//set the mock as default
|
||||
$mock->expects($this->any())
|
||||
->method('isActiveByDefault')
|
||||
->will($this->returnValue(TRUE));
|
||||
|
||||
|
||||
$mock->expects($this->any())
|
||||
->method('getOrder')
|
||||
->will($this->returnValue($order));
|
||||
|
||||
|
||||
//set the return value
|
||||
$mock->expects($this->any())
|
||||
->method('renderResult')
|
||||
->will($this->returnValue($result));
|
||||
|
||||
|
||||
return $mock;
|
||||
}
|
||||
|
||||
|
||||
private function createNonDefaultDomainSearchService($result, $order, $domain)
|
||||
{
|
||||
$mock = $this
|
||||
->getMockForAbstractClass('Chill\MainBundle\Search\AbstractSearch');
|
||||
|
||||
|
||||
//set the mock as default
|
||||
$mock->expects($this->any())
|
||||
->method('isActiveByDefault')
|
||||
->will($this->returnValue(FALSE));
|
||||
|
||||
|
||||
$mock->expects($this->any())
|
||||
->method('getOrder')
|
||||
->will($this->returnValue($order));
|
||||
|
||||
|
||||
$mock->expects($this->any())
|
||||
->method('supports')
|
||||
->will($this->returnValue($domain));
|
||||
|
||||
|
||||
//set the return value
|
||||
$mock->expects($this->any())
|
||||
->method('renderResult')
|
||||
->will($this->returnValue($result));
|
||||
|
||||
|
||||
return $mock;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Search\Utils;
|
||||
|
||||
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExtractDateFromPatternTest extends TestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* @dataProvider provideSubjects
|
||||
*/
|
||||
public function testExtractDate(string $subject, string $filtered, int $count, ...$datesSearched)
|
||||
{
|
||||
$extractor = new ExtractDateFromPattern();
|
||||
$result = $extractor->extractDates($subject);
|
||||
|
||||
$this->assertCount($count, $result->getFound());
|
||||
$this->assertEquals($filtered, $result->getFilteredSubject());
|
||||
$this->assertContainsOnlyInstancesOf(\DateTimeImmutable::class, $result->getFound());
|
||||
|
||||
$dates = \array_map(
|
||||
function (\DateTimeImmutable $d) {
|
||||
return $d->format('Y-m-d');
|
||||
}, $result->getFound()
|
||||
);
|
||||
|
||||
foreach ($datesSearched as $date) {
|
||||
$this->assertContains($date, $dates);
|
||||
}
|
||||
}
|
||||
|
||||
public function provideSubjects()
|
||||
{
|
||||
yield ["15/06/1981", "", 1, '1981-06-15'];
|
||||
yield ["15/06/1981 30/12/1987", "", 2, '1981-06-15', '1987-12-30'];
|
||||
yield ["diallo 15/06/1981", "diallo", 1, '1981-06-15'];
|
||||
yield ["diallo 31/03/1981", "diallo", 1, '1981-03-31'];
|
||||
yield ["diallo 15-06-1981", "diallo", 1, '1981-06-15'];
|
||||
yield ["diallo 1981-12-08", "diallo", 1, '1981-12-08'];
|
||||
yield ["diallo", "diallo", 0];
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Search\Utils;
|
||||
|
||||
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
class ExtractPhonenumberFromPatternTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideData
|
||||
*/
|
||||
public function testExtract($subject, $expectedCount, $expected, $filteredSubject, $msg)
|
||||
{
|
||||
$extractor = new ExtractPhonenumberFromPattern();
|
||||
$result = $extractor->extractPhonenumber($subject);
|
||||
|
||||
$this->assertCount($expectedCount, $result->getFound());
|
||||
$this->assertEquals($filteredSubject, $result->getFilteredSubject());
|
||||
$this->assertEquals($expected, $result->getFound());
|
||||
}
|
||||
|
||||
public function provideData()
|
||||
{
|
||||
yield ['Diallo', 0, [], 'Diallo', "no phonenumber"];
|
||||
yield ['Diallo 15/06/2021', 0, [], 'Diallo 15/06/2021', "no phonenumber and a date"];
|
||||
yield ['Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo', "a phonenumber and a name"];
|
||||
yield ['Diallo 123 456', 1, ['123456'], 'Diallo', "a number and a name, without leadiing 0"];
|
||||
yield ['123 456', 1, ['123456'], '', "only phonenumber"];
|
||||
yield ['0123 456', 1, ['+32123456'], '', "only phonenumber with a leading 0"];
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Serializer\Normalizer;
|
||||
|
||||
use Chill\MainBundle\Serializer\Normalizer\DateNormalizer;
|
||||
use Prophecy\Prophet;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class DateNormalizerTest extends KernelTestCase
|
||||
{
|
||||
private Prophet $prophet;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->prophet = new Prophet();
|
||||
}
|
||||
|
||||
public function testSupports()
|
||||
{
|
||||
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTime(), 'json'));
|
||||
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTimeImmutable(), 'json'));
|
||||
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTime(), 'docgen'));
|
||||
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTimeImmutable(), 'docgen'));
|
||||
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(null, 'docgen', ['docgen:expects' => \DateTimeImmutable::class]));
|
||||
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(null, 'docgen', ['docgen:expects' => \DateTimeInterface::class]));
|
||||
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(null, 'docgen', ['docgen:expects' => \DateTime::class]));
|
||||
$this->assertFalse($this->buildDateNormalizer()->supportsNormalization(new \stdClass(), 'docgen'));
|
||||
$this->assertFalse($this->buildDateNormalizer()->supportsNormalization(new \DateTime(), 'xml'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider generateDataNormalize
|
||||
*/
|
||||
public function testNormalize($expected, $date, $format, $locale, $msg)
|
||||
{
|
||||
$this->assertEquals($expected, $this->buildDateNormalizer($locale)->normalize($date, $format, []), $msg);
|
||||
}
|
||||
|
||||
private function buildDateNormalizer(string $locale = null): DateNormalizer
|
||||
{
|
||||
$requestStack = $this->prophet->prophesize(RequestStack::class);
|
||||
$parameterBag = new ParameterBag();
|
||||
$parameterBag->set('kernel.default_locale', 'fr');
|
||||
|
||||
if ($locale === null) {
|
||||
$requestStack->getCurrentRequest()->willReturn(null);
|
||||
} else {
|
||||
$request = $this->prophet->prophesize(Request::class);
|
||||
$request->getLocale()->willReturn($locale);
|
||||
$requestStack->getCurrentRequest()->willReturn($request->reveal());
|
||||
}
|
||||
|
||||
return new DateNormalizer($requestStack->reveal(), $parameterBag);
|
||||
}
|
||||
|
||||
public function generateDataNormalize()
|
||||
{
|
||||
$datetime = \DateTime::createFromFormat('Y-m-d H:i:sO', '2021-06-05 15:05:01+02:00');
|
||||
$date = \DateTime::createFromFormat('Y-m-d H:i:sO', '2021-06-05 00:00:00+02:00');
|
||||
yield [
|
||||
['datetime' => '2021-06-05T15:05:01+0200'],
|
||||
$datetime, 'json', null, 'simple normalization to json'
|
||||
];
|
||||
|
||||
yield [
|
||||
['long' => '5 juin 2021', 'short' => '05/06/2021'],
|
||||
$date, 'docgen', 'fr', 'normalization to docgen for a date, with current request'
|
||||
];
|
||||
|
||||
yield [
|
||||
['long' => '5 juin 2021', 'short' => '05/06/2021'],
|
||||
$date, 'docgen', null, 'normalization to docgen for a date, without current request'
|
||||
];
|
||||
|
||||
yield [
|
||||
['long' => '5 juin 2021 à 15:05', 'short' => '05/06/2021 15:05'],
|
||||
$datetime, 'docgen', null, 'normalization to docgen for a datetime, without current request'
|
||||
];
|
||||
|
||||
yield [
|
||||
['long' => '', 'short' => ''],
|
||||
null, 'docgen', null, 'normalization to docgen for a null datetime'
|
||||
];
|
||||
}
|
||||
}
|
@@ -128,4 +128,16 @@ services:
|
||||
|
||||
Chill\MainBundle\Form\DataTransform\AddressToIdDataTransformer: ~
|
||||
|
||||
Chill\MainBundle\Form\DataTransform\AddressToIdDataTransformer:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
Chill\MainBundle\Form\LocationFormType:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\MainBundle\Form\UserCurrentLocationType:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\MainBundle\Form\Type\LocationFormType: ~
|
||||
|
@@ -7,8 +7,8 @@ services:
|
||||
resource: '../../Routing/MenuBuilder'
|
||||
|
||||
Chill\MainBundle\Routing\MenuBuilder\UserMenuBuilder:
|
||||
arguments:
|
||||
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: 'chill.menu_builder' }
|
||||
|
||||
|
@@ -8,7 +8,16 @@ services:
|
||||
|
||||
Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider'
|
||||
|
||||
Chill\MainBundle\Search\SearchApi: ~
|
||||
Chill\MainBundle\Search\SearchApi:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
arguments:
|
||||
$providers: !tagged_iterator chill.search_api_provider
|
||||
|
||||
Chill\MainBundle\Search\Entity\:
|
||||
resource: '../../Search/Entity'
|
||||
|
||||
Chill\MainBundle\Search\Utils\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: './../Search/Utils/'
|
||||
|
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Add current location to User entity
|
||||
*/
|
||||
final class Version20211116162847 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add current location to User entity';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE users ADD currentLocation_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E93C219753 FOREIGN KEY (currentLocation_id) REFERENCES chill_main_location (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_1483A5E93C219753 ON users (currentLocation_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE users DROP CONSTRAINT FK_1483A5E93C219753');
|
||||
$this->addSql('DROP INDEX IDX_1483A5E93C219753');
|
||||
$this->addSql('ALTER TABLE users DROP currentLocation_id');
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20211119173554 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove comment on deprecated json_array type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$columns = [
|
||||
'users.attributes'
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$this->addSql("COMMENT ON COLUMN $col IS NULL");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->throwIrreversibleMigrationException();
|
||||
}
|
||||
}
|
@@ -86,6 +86,10 @@ address more:
|
||||
Create a new address: Créer une nouvelle adresse
|
||||
Create an address: Créer une adresse
|
||||
Update address: Modifier l'adresse
|
||||
City or postal code: Ville ou code postal
|
||||
|
||||
# contact
|
||||
Part of the phonenumber: Partie du numéro de téléphone
|
||||
|
||||
#serach
|
||||
Your search is empty. Please provide search terms.: La recherche est vide. Merci de fournir des termes de recherche.
|
||||
@@ -176,6 +180,13 @@ Flags: Drapeaux
|
||||
# admin section for users jobs
|
||||
User jobs: Métiers
|
||||
|
||||
# user page for current location
|
||||
Current location: Localisation actuelle
|
||||
Edit my current location: Éditer ma localisation actuelle
|
||||
Change current location: Changer ma localisation actuelle
|
||||
Set a location: Indiquer une localisation
|
||||
Current location successfully updated: Localisation actuelle mise à jour
|
||||
Pick a location: Choisir un lieu
|
||||
|
||||
#admin section for circles (old: scopes)
|
||||
List circles: Cercles
|
||||
@@ -190,7 +201,7 @@ Location: Localisation
|
||||
Location type list: Liste des types de localisation
|
||||
Create a new location type: Créer un nouveau type de localisation
|
||||
Available for users: Disponible aux utilisateurs
|
||||
Address required: Adresse requise?
|
||||
Address required: Adresse requise?
|
||||
Contact data: Données de contact?
|
||||
optional: optionnel
|
||||
required: requis
|
||||
@@ -200,8 +211,7 @@ Location list: Liste des localisations
|
||||
Location type: Type de localisation
|
||||
Phonenumber1: Numéro de téléphone
|
||||
Phonenumber2: Autre numéro de téléphone
|
||||
Configure location: Configuration des localisations
|
||||
Configure location type: Configuration des types de localisations
|
||||
Configure location and location type: Configuration des localisations
|
||||
|
||||
# circles / scopes
|
||||
Choose the circle: Choisir le cercle
|
||||
|
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\DataFixtures\Helper;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
trait RandomPersonHelperTrait
|
||||
{
|
||||
private ?int $nbOfPersons = null;
|
||||
|
||||
protected function getRandomPerson(EntityManagerInterface $em): Person
|
||||
{
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb
|
||||
->from(Person::class, 'p')
|
||||
;
|
||||
|
||||
if (null === $this->nbOfPersons) {
|
||||
$this->nbOfPersons = $qb
|
||||
->select('COUNT(p)')
|
||||
->getQuery()
|
||||
->getSingleScalarResult()
|
||||
;
|
||||
}
|
||||
|
||||
return $qb
|
||||
->select('p')
|
||||
->setMaxResults(1)
|
||||
->setFirstResult(\random_int(0, $this->nbOfPersons))
|
||||
->getQuery()
|
||||
->getSingleResult()
|
||||
;
|
||||
}
|
||||
}
|
@@ -86,13 +86,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
|
||||
$loader->load('services/security.yaml');
|
||||
$loader->load('services/doctrineEventListener.yaml');
|
||||
|
||||
// load service advanced search only if configure
|
||||
if ($config['search']['search_by_phone'] != 'never') {
|
||||
$loader->load('services/search_by_phone.yaml');
|
||||
$container->setParameter('chill_person.search.search_by_phone',
|
||||
$config['search']['search_by_phone']);
|
||||
}
|
||||
|
||||
if ($container->getParameter('chill_person.accompanying_period') !== 'hidden') {
|
||||
$loader->load('services/exports_accompanying_period.yaml');
|
||||
}
|
||||
|
@@ -27,19 +27,6 @@ class Configuration implements ConfigurationInterface
|
||||
$rootNode
|
||||
->canBeDisabled()
|
||||
->children()
|
||||
->arrayNode('search')
|
||||
->canBeDisabled()
|
||||
->children()
|
||||
->enumNode('search_by_phone')
|
||||
->values(['always', 'on-domain', 'never'])
|
||||
->defaultValue('on-domain')
|
||||
->info('enable search by phone. \'always\' show the result '
|
||||
. 'on every result. \'on-domain\' will show the result '
|
||||
. 'only if the domain is given in the search box. '
|
||||
. '\'never\' disable this feature')
|
||||
->end()
|
||||
->end() //children for 'search', parent = array node 'search'
|
||||
->end() // array 'search', parent = children of root
|
||||
->arrayNode('validation')
|
||||
->canBeDisabled()
|
||||
->children()
|
||||
|
@@ -1139,11 +1139,11 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
public function getGroupSequence()
|
||||
{
|
||||
if ($this->getStep() == self::STEP_DRAFT) {
|
||||
if ($this->getStep() == self::STEP_DRAFT)
|
||||
{
|
||||
return [[self::STEP_DRAFT]];
|
||||
}
|
||||
|
||||
if ($this->getStep() == self::STEP_CONFIRMED) {
|
||||
} elseif ($this->getStep() == self::STEP_CONFIRMED)
|
||||
{
|
||||
return [[self::STEP_DRAFT, self::STEP_CONFIRMED]];
|
||||
}
|
||||
|
||||
|
@@ -434,6 +434,5 @@ class Household
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
dump($cond);
|
||||
}
|
||||
}
|
||||
|
@@ -37,20 +37,20 @@ class MaritalStatus
|
||||
* @ORM\Id()
|
||||
* @ORM\Column(type="string", length=7)
|
||||
*/
|
||||
private $id;
|
||||
private ?string $id;
|
||||
|
||||
/**
|
||||
* @var string array
|
||||
* @ORM\Column(type="json")
|
||||
*/
|
||||
private $name;
|
||||
private array $name;
|
||||
|
||||
/**
|
||||
* Get id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class MaritalStatus
|
||||
* @param string $id
|
||||
* @return MaritalStatus
|
||||
*/
|
||||
public function setId($id)
|
||||
public function setId(string $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
@@ -73,7 +73,7 @@ class MaritalStatus
|
||||
* @param string array $name
|
||||
* @return MaritalStatus
|
||||
*/
|
||||
public function setName($name)
|
||||
public function setName(array $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
@@ -85,7 +85,7 @@ class MaritalStatus
|
||||
*
|
||||
* @return string array
|
||||
*/
|
||||
public function getName()
|
||||
public function getName(): array
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
@@ -39,10 +39,16 @@ use DateTimeInterface;
|
||||
*
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="chill_person_person",
|
||||
* indexes={@ORM\Index(
|
||||
* indexes={
|
||||
* @ORM\Index(
|
||||
* name="person_names",
|
||||
* columns={"firstName", "lastName"}
|
||||
* )})
|
||||
* ),
|
||||
* @ORM\Index(
|
||||
* name="person_birthdate",
|
||||
* columns={"birthdate"}
|
||||
* )
|
||||
* })
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
* @DiscriminatorMap(typeProperty="type", mapping={
|
||||
* "person"=Person::class
|
||||
@@ -213,7 +219,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private ?\DateTime $maritalStatusDate;
|
||||
private ?\DateTime $maritalStatusDate = null;
|
||||
|
||||
/**
|
||||
* Comment on marital status
|
||||
@@ -246,7 +252,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* The person's phonenumber
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="text", length=40, nullable=true)
|
||||
* @ORM\Column(type="text")
|
||||
* @Assert\Regex(
|
||||
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
|
||||
* groups={"general", "creation"}
|
||||
@@ -256,13 +262,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $phonenumber = '';
|
||||
private string $phonenumber = '';
|
||||
|
||||
/**
|
||||
* The person's mobile phone number
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="text", length=40, nullable=true)
|
||||
* @ORM\Column(type="text")
|
||||
* @Assert\Regex(
|
||||
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
|
||||
* groups={"general", "creation"}
|
||||
@@ -272,7 +278,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* groups={"general", "creation"}
|
||||
* )
|
||||
*/
|
||||
private $mobilenumber = '';
|
||||
private string $mobilenumber = '';
|
||||
|
||||
/**
|
||||
* @var Collection
|
||||
@@ -1088,9 +1094,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
/**
|
||||
* Get nationality
|
||||
*
|
||||
* @return Chill\MainBundle\Entity\Country
|
||||
* @return Country
|
||||
*/
|
||||
public function getNationality()
|
||||
public function getNationality(): ?Country
|
||||
{
|
||||
return $this->nationality;
|
||||
}
|
||||
@@ -1170,7 +1176,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhonenumber()
|
||||
public function getPhonenumber(): string
|
||||
{
|
||||
return $this->phonenumber;
|
||||
}
|
||||
@@ -1193,7 +1199,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMobilenumber()
|
||||
public function getMobilenumber(): string
|
||||
{
|
||||
return $this->mobilenumber;
|
||||
}
|
||||
|
@@ -9,7 +9,10 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
* Person Phones
|
||||
*
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="chill_person_phone")
|
||||
* @ORM\Table(name="chill_person_phone",
|
||||
* indexes={
|
||||
* @ORM\Index(name="phonenumber", columns={"phonenumber"})
|
||||
* })
|
||||
*/
|
||||
class PersonPhone
|
||||
{
|
||||
@@ -107,7 +110,7 @@ class PersonPhone
|
||||
{
|
||||
$this->date = $date;
|
||||
}
|
||||
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->getDescription()) && empty($this->getPhonenumber());
|
||||
|
@@ -31,7 +31,7 @@ class GenderType extends AbstractType {
|
||||
'choices' => $a,
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
'placeholder' => null
|
||||
'placeholder' => null,
|
||||
));
|
||||
}
|
||||
|
||||
|
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace Chill\PersonBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\CountryRepository;
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\MainBundle\Search\SearchApi;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
@@ -49,125 +53,114 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): array {
|
||||
$qb = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$query = $this->buildAuthorizedQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode);
|
||||
$this->addACLClauses($qb, 'p');
|
||||
$countryCode, $phonenumber, $city);
|
||||
|
||||
return $this->getQueryResult($qb, 'p', $simplify, $limit, $start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to prepare and return the search query for PersonACL.
|
||||
*
|
||||
* This method replace the select clause with required parameters, depending on the
|
||||
* "simplify" parameter. It also add query limits.
|
||||
*
|
||||
* The given alias must represent the person alias.
|
||||
*
|
||||
* @return array|Person[]
|
||||
*/
|
||||
public function getQueryResult(QueryBuilder $qb, string $alias, bool $simplify, int $limit, int $start): array
|
||||
{
|
||||
if ($simplify) {
|
||||
$qb->select(
|
||||
$alias.'.id',
|
||||
$qb->expr()->concat(
|
||||
$alias.'.firstName',
|
||||
$qb->expr()->literal(' '),
|
||||
$alias.'.lastName'
|
||||
).'AS text'
|
||||
);
|
||||
} else {
|
||||
$qb->select($alias);
|
||||
}
|
||||
|
||||
$qb
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($start);
|
||||
|
||||
//order by firstname, lastname
|
||||
$qb
|
||||
->orderBy($alias.'.firstName')
|
||||
->addOrderBy($alias.'.lastName');
|
||||
|
||||
if ($simplify) {
|
||||
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
|
||||
} else {
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
return $this->fetchQueryPerson($query);
|
||||
}
|
||||
|
||||
public function countBySearchCriteria(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): int {
|
||||
$qb = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$query = $this->buildAuthorizedQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode);
|
||||
$this->addACLClauses($qb, 'p');
|
||||
$countryCode, $phonenumber, $city)
|
||||
;
|
||||
|
||||
return $this->getCountQueryResult($qb,'p');
|
||||
return $this->fetchQueryCount($query);
|
||||
}
|
||||
|
||||
public function fetchQueryCount(SearchApiQuery $query): int
|
||||
{
|
||||
$rsm = new Query\ResultSetMapping();
|
||||
$rsm->addScalarResult('c', 'c');
|
||||
|
||||
$nql = $this->em->createNativeQuery($query->buildQuery(true), $rsm);
|
||||
$nql->setParameters($query->buildParameters(true));
|
||||
|
||||
return $nql->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to prepare and return the count for search query
|
||||
*
|
||||
* This method replace the select clause with required parameters, depending on the
|
||||
* "simplify" parameter.
|
||||
*
|
||||
* The given alias must represent the person alias in the query builder.
|
||||
* @return array|Person[]
|
||||
*/
|
||||
public function getCountQueryResult(QueryBuilder $qb, $alias): int
|
||||
public function fetchQueryPerson(SearchApiQuery $query, ?int $start = 0, ?int $limit = 50): array
|
||||
{
|
||||
$qb->select('COUNT('.$alias.'.id)');
|
||||
$rsm = new Query\ResultSetMappingBuilder($this->em);
|
||||
$rsm->addRootEntityFromClassMetadata(Person::class, 'person');
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
$query->addSelectClause($rsm->generateSelectClause());
|
||||
|
||||
$nql = $this->em->createNativeQuery(
|
||||
$query->buildQuery()." ORDER BY pertinence DESC OFFSET ? LIMIT ?", $rsm
|
||||
)->setParameters(\array_merge($query->buildParameters(), [$start, $limit]));
|
||||
|
||||
return $nql->getResult();
|
||||
}
|
||||
|
||||
public function findBySimilaritySearch(string $pattern, int $firstResult,
|
||||
int $maxResult, bool $simplify = false)
|
||||
{
|
||||
$qb = $this->createSimilarityQuery($pattern);
|
||||
$this->addACLClauses($qb, 'sp');
|
||||
public function buildAuthorizedQuery(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): SearchApiQuery {
|
||||
$query = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode, $phonenumber)
|
||||
;
|
||||
|
||||
return $this->getQueryResult($qb, 'sp', $simplify, $maxResult, $firstResult);
|
||||
return $this->addAuthorizations($query);
|
||||
}
|
||||
|
||||
public function countBySimilaritySearch(string $pattern)
|
||||
private function addAuthorizations(SearchApiQuery $query): SearchApiQuery
|
||||
{
|
||||
$qb = $this->createSimilarityQuery($pattern);
|
||||
$this->addACLClauses($qb, 'sp');
|
||||
$authorizedCenters = $this->authorizationHelper
|
||||
->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
|
||||
|
||||
return $this->getCountQueryResult($qb, 'sp');
|
||||
if ([] === $authorizedCenters) {
|
||||
return $query->andWhereClause("FALSE = TRUE", []);
|
||||
}
|
||||
|
||||
return $query
|
||||
->andWhereClause(
|
||||
strtr(
|
||||
"person.center_id IN ({{ center_ids }})",
|
||||
[
|
||||
'{{ center_ids }}' => \implode(', ',
|
||||
\array_fill(0, count($authorizedCenters), '?')),
|
||||
]
|
||||
),
|
||||
\array_map(function(Center $c) {return $c->getId();}, $authorizedCenters)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a search query without ACL
|
||||
*
|
||||
* The person alias is a "p"
|
||||
*
|
||||
* @param string|null $default
|
||||
* @param string|null $firstname
|
||||
* @param string|null $lastname
|
||||
* @param \DateTime|null $birthdate
|
||||
* @param \DateTime|null $birthdateBefore
|
||||
* @param \DateTime|null $birthdateAfter
|
||||
* @param string|null $gender
|
||||
* @param string|null $countryCode
|
||||
* @return QueryBuilder
|
||||
* @throws NonUniqueResultException
|
||||
* @throws ParsingException
|
||||
*/
|
||||
@@ -175,118 +168,107 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
): QueryBuilder {
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): SearchApiQuery {
|
||||
$query = new SearchApiQuery();
|
||||
$query
|
||||
->setFromClause("chill_person_person AS person")
|
||||
;
|
||||
|
||||
if (!$this->security->getUser() instanceof User) {
|
||||
throw new \RuntimeException("Search must be performed by a valid user");
|
||||
}
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->from(Person::class, 'p');
|
||||
$pertinence = [];
|
||||
$pertinenceArgs = [];
|
||||
$orWhereSearchClause = [];
|
||||
$orWhereSearchClauseArgs = [];
|
||||
|
||||
if (NULL !== $firstname) {
|
||||
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname'))
|
||||
->setParameter('firstname', '%'.$firstname.'%');
|
||||
}
|
||||
if ("" !== $default) {
|
||||
foreach (\explode(" ", $default) as $str) {
|
||||
$pertinence[] =
|
||||
"STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ".
|
||||
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + ".
|
||||
"(EXISTS (SELECT 1 FROM unnest(string_to_array(fullnamecanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int";
|
||||
\array_push($pertinenceArgs, $str, $str, $str);
|
||||
|
||||
if (NULL !== $lastname) {
|
||||
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname'))
|
||||
->setParameter('lastname', '%'.$lastname.'%');
|
||||
$orWhereSearchClause[] =
|
||||
"(LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
|
||||
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' )";
|
||||
\array_push($orWhereSearchClauseArgs, $str, $str);
|
||||
}
|
||||
|
||||
$query->andWhereClause(\implode(' OR ', $orWhereSearchClause),
|
||||
$orWhereSearchClauseArgs);
|
||||
} else {
|
||||
$pertinence = ["1"];
|
||||
$pertinenceArgs = [];
|
||||
}
|
||||
$query
|
||||
->setSelectPertinence(\implode(' + ', $pertinence), $pertinenceArgs)
|
||||
;
|
||||
|
||||
if (NULL !== $birthdate) {
|
||||
$qb->andWhere($qb->expr()->eq('p.birthdate', ':birthdate'))
|
||||
->setParameter('birthdate', $birthdate);
|
||||
$query->andWhereClause(
|
||||
"person.birthdate = ?::date",
|
||||
[$birthdate->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $birthdateAfter) {
|
||||
$qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter'))
|
||||
->setParameter('birthdateafter', $birthdateAfter);
|
||||
if (NULL !== $firstname) {
|
||||
$query->andWhereClause(
|
||||
"UNACCENT(LOWER(person.firstname)) LIKE '%' || UNACCENT(LOWER(?)) || '%'",
|
||||
[$firstname]
|
||||
);
|
||||
}
|
||||
if (NULL !== $lastname) {
|
||||
$query->andWhereClause(
|
||||
"UNACCENT(LOWER(person.lastname)) LIKE '%' || UNACCENT(LOWER(?)) || '%'",
|
||||
[$lastname]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $birthdateBefore) {
|
||||
$qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore'))
|
||||
->setParameter('birthdatebefore', $birthdateBefore);
|
||||
$query->andWhereClause(
|
||||
'p.birthdate < ?::date',
|
||||
[$birthdateBefore->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $gender) {
|
||||
$qb->andWhere($qb->expr()->eq('p.gender', ':gender'))
|
||||
->setParameter('gender', $gender);
|
||||
if (NULL !== $birthdateAfter) {
|
||||
$query->andWhereClause(
|
||||
'p.birthdate > ?::date',
|
||||
[$birthdateAfter->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $countryCode) {
|
||||
try {
|
||||
$country = $this->countryRepository->findOneBy(['countryCode' => $countryCode]);
|
||||
} catch (NoResultException $ex) {
|
||||
throw new ParsingException('The country code "'.$countryCode.'" '
|
||||
. ', used in nationality, is unknow', 0, $ex);
|
||||
} catch (NonUniqueResultException $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('p.nationality', ':nationality'))
|
||||
->setParameter('nationality', $country);
|
||||
if (NULL !== $phonenumber) {
|
||||
$query->andWhereClause(
|
||||
"person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR pp.phonenumber LIKE '%' || ? || '%'"
|
||||
,
|
||||
[$phonenumber, $phonenumber, $phonenumber]
|
||||
);
|
||||
$query->setFromClause($query->getFromClause()." LEFT JOIN chill_person_phone pp ON pp.person_id = person.id");
|
||||
}
|
||||
if (null !== $city) {
|
||||
$query->setFromClause($query->getFromClause()." ".
|
||||
"JOIN view_chill_person_current_address vcpca ON vcpca.person_id = person.id ".
|
||||
"JOIN chill_main_address cma ON vcpca.address_id = cma.id ".
|
||||
"JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id");
|
||||
|
||||
if (NULL !== $default) {
|
||||
$grams = explode(' ', $default);
|
||||
|
||||
foreach($grams as $key => $gram) {
|
||||
$qb->andWhere($qb->expr()
|
||||
->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))'))
|
||||
->setParameter('default_'.$key, '%'.$gram.'%');
|
||||
foreach (\explode(" ", $city) as $cityStr) {
|
||||
$query->andWhereClause(
|
||||
"(UNACCENT(LOWER(cmpc.label)) LIKE '%' || UNACCENT(LOWER(?)) || '%' OR cmpc.code LIKE '%' || UNACCENT(LOWER(?)) || '%')",
|
||||
[$cityStr, $city]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function addACLClauses(QueryBuilder $qb, string $personAlias): void
|
||||
{
|
||||
// restrict center for security
|
||||
$reachableCenters = $this->authorizationHelper
|
||||
->getReachableCenters($this->security->getUser(), 'CHILL_PERSON_SEE');
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()
|
||||
->in($personAlias.'.center', ':centers'),
|
||||
$qb->expr()
|
||||
->isNull($personAlias.'.center')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('centers', $reachableCenters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query for searching by similarity.
|
||||
*
|
||||
* The person alias is "sp".
|
||||
*
|
||||
* @param $pattern
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function createSimilarityQuery($pattern): QueryBuilder
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
|
||||
$qb->from(Person::class, 'sp');
|
||||
|
||||
$grams = explode(' ', $pattern);
|
||||
|
||||
foreach($grams as $key => $gram) {
|
||||
$qb->andWhere('STRICT_WORD_SIMILARITY_OPS(:default_'.$key.', sp.fullnameCanonical) = TRUE')
|
||||
->setParameter('default_'.$key, '%'.$gram.'%');
|
||||
|
||||
// remove the perfect matches
|
||||
$qb->andWhere($qb->expr()
|
||||
->notLike('sp.fullnameCanonical', 'UNACCENT(LOWER(:not_default_'.$key.'))'))
|
||||
->setParameter('not_default_'.$key, '%'.$gram.'%');
|
||||
if (null !== $countryCode) {
|
||||
$query->setFromClause($query->getFromClause()." JOIN country ON person.nationality_id = country.id");
|
||||
$query->andWhereClause("country.countrycode = UPPER(?)", [$countryCode]);
|
||||
}
|
||||
if (null !== $gender) {
|
||||
$query->andWhereClause("person.gender = ?", [$gender]);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
@@ -3,16 +3,14 @@
|
||||
namespace Chill\PersonBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
|
||||
interface PersonACLAwareRepositoryInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @return array|Person[]
|
||||
* @throws NonUniqueResultException
|
||||
* @throws ParsingException
|
||||
*/
|
||||
public function findBySearchCriteria(
|
||||
int $start,
|
||||
@@ -21,30 +19,38 @@ interface PersonACLAwareRepositoryInterface
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): array;
|
||||
|
||||
public function countBySearchCriteria(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
);
|
||||
|
||||
public function findBySimilaritySearch(
|
||||
string $pattern,
|
||||
int $firstResult,
|
||||
int $maxResult,
|
||||
bool $simplify = false
|
||||
);
|
||||
|
||||
public function countBySimilaritySearch(string $pattern);
|
||||
public function buildAuthorizedQuery(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): SearchApiQuery;
|
||||
}
|
||||
|
@@ -60,6 +60,16 @@ h2.badge-title {
|
||||
h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
//position: relative;
|
||||
span {
|
||||
display: none;
|
||||
//position: absolute;
|
||||
//top: 0;
|
||||
//left: 0;
|
||||
//transform: rotate(270deg);
|
||||
//transform-origin: 0 0;
|
||||
}
|
||||
}
|
||||
span.title_action {
|
||||
flex-grow: 1;
|
||||
@@ -117,3 +127,36 @@ div.activity-list {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AccompanyingCourse: HeaderSlider Carousel
|
||||
div#header-accompanying_course-details {
|
||||
button.carousel-control-prev,
|
||||
button.carousel-control-next {
|
||||
width: 8%;
|
||||
opacity: inherit;
|
||||
}
|
||||
button.carousel-control-prev {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
span.to-social-issues,
|
||||
span.to-persons-associated {
|
||||
display: inline-block;
|
||||
border-radius: 15px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
box-shadow: 0 0 3px 1px grey;
|
||||
opacity: 0.8;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
span.to-social-issues {
|
||||
background-color: #4bafe8;
|
||||
border-left: 12px solid #32749a;
|
||||
}
|
||||
span.to-persons-associated {
|
||||
background-color: #16d9b4;
|
||||
border-right: 12px solid #ffffff;
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
///
|
||||
|
||||
@mixin chill_badge($color) {
|
||||
text-transform: capitalize !important;
|
||||
//text-transform: capitalize !important;
|
||||
font-weight: 500 !important;
|
||||
border-left: 20px groove $color;
|
||||
&:before {
|
||||
|
@@ -22,20 +22,22 @@
|
||||
<i>{{ $t('course.open_at') }}{{ $d(accompanyingCourse.openingDate.datetime, 'text') }}</i>
|
||||
</span>
|
||||
<span v-if="accompanyingCourse.user" class="d-md-block ms-3 ms-md-0">
|
||||
<abbr :title="$t('course.referrer')">{{ $t('course.referrer') }}:</abbr> <b>{{ accompanyingCourse.user.username }}</b>
|
||||
<span class="item-key">{{ $t('course.referrer') }}:</span> <b>{{ accompanyingCourse.user.username }}</b>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</teleport>
|
||||
|
||||
<teleport to="#header-accompanying_course-details #banner-social-issues">
|
||||
<div class="col-12">
|
||||
<social-issue
|
||||
v-for="issue in accompanyingCourse.socialIssues"
|
||||
v-bind:key="issue.id"
|
||||
v-bind:issue="issue">
|
||||
</social-issue>
|
||||
</div>
|
||||
<social-issue
|
||||
v-for="issue in accompanyingCourse.socialIssues"
|
||||
v-bind:key="issue.id"
|
||||
v-bind:issue="issue">
|
||||
</social-issue>
|
||||
</teleport>
|
||||
|
||||
<teleport to="#header-accompanying_course-details #banner-persons-associated">
|
||||
<persons-associated :accompanyingCourse="accompanyingCourse"></persons-associated>
|
||||
</teleport>
|
||||
|
||||
</template>
|
||||
@@ -43,12 +45,14 @@
|
||||
<script>
|
||||
import ToggleFlags from './Banner/ToggleFlags';
|
||||
import SocialIssue from './Banner/SocialIssue.vue';
|
||||
import PersonsAssociated from './Banner/PersonsAssociated.vue';
|
||||
|
||||
export default {
|
||||
name: 'Banner',
|
||||
components: {
|
||||
ToggleFlags,
|
||||
SocialIssue
|
||||
SocialIssue,
|
||||
PersonsAssociated
|
||||
},
|
||||
computed: {
|
||||
accompanyingCourse() {
|
||||
|
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<span v-for="h in personsByHousehold()" :class="{ 'household': householdExists(h.id), 'no-household': !householdExists(h.id) }">
|
||||
<a v-if="householdExists(h.id)" :href="householdLink(h.id)">
|
||||
<i class="fa fa-home fa-fw text-light" :title="$t('persons_associated.show_household_number', { id: h.id })"></i>
|
||||
</a>
|
||||
<span v-for="person in h.persons" class="me-1">
|
||||
<on-the-fly :type="person.type" :id="person.id" :buttonText="person.text" :displayBadge="'true' === 'true'" action="show"></on-the-fly>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly'
|
||||
|
||||
export default {
|
||||
name: "PersonsAssociated",
|
||||
components: {
|
||||
OnTheFly
|
||||
},
|
||||
props: [ 'accompanyingCourse' ],
|
||||
computed: {
|
||||
participations() {
|
||||
return this.accompanyingCourse.participations.filter(p => p.endDate === null)
|
||||
},
|
||||
persons() {
|
||||
return this.participations.map(p => p.person)
|
||||
},
|
||||
resources() {
|
||||
return this.accompanyingCourse.resources
|
||||
},
|
||||
requestor() {
|
||||
return this.accompanyingCourse.requestor
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
uniq(array) {
|
||||
return [...new Set(array)]
|
||||
},
|
||||
personsByHousehold() {
|
||||
|
||||
let households = []
|
||||
this.persons.forEach(p => { households.push(p.current_household_id) })
|
||||
|
||||
let personsByHousehold = []
|
||||
this.uniq(households).forEach(h => {
|
||||
personsByHousehold.push({
|
||||
id: h !== null ? h : 0,
|
||||
persons: this.persons.filter(p => p.current_household_id === h)
|
||||
})
|
||||
})
|
||||
console.log(personsByHousehold)
|
||||
|
||||
|
||||
return personsByHousehold
|
||||
},
|
||||
householdExists(id) {
|
||||
return id !== 0
|
||||
},
|
||||
householdLink(id) {
|
||||
return `/fr/person/household/${id}/summary`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
span.household {
|
||||
display: inline-block;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
margin-right: 0.3em;
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
@@ -10,7 +10,7 @@
|
||||
<div class="alert alert-warning">
|
||||
{{ $t('confirm.alert_validation') }}
|
||||
<ul class="mt-2">
|
||||
<li v-for="k in validationKeys">
|
||||
<li v-for="k in validationKeys" :key=k>
|
||||
{{ $t(notValidMessages[k].msg) }}
|
||||
<a :href="notValidMessages[k].anchor">
|
||||
<i class="fa fa-level-up fa-fw"></i>
|
||||
@@ -83,7 +83,11 @@ export default {
|
||||
},
|
||||
location: {
|
||||
msg: 'confirm.location_not_valid',
|
||||
anchor: '#section-20' //
|
||||
anchor: '#section-20'
|
||||
},
|
||||
origin: {
|
||||
msg: 'confirm.origin_not_valid',
|
||||
anchor: '#section-30'
|
||||
},
|
||||
socialIssue: {
|
||||
msg: 'confirm.socialIssue_not_valid',
|
||||
@@ -103,6 +107,7 @@ export default {
|
||||
...mapGetters([
|
||||
'isParticipationValid',
|
||||
'isSocialIssueValid',
|
||||
'isOriginValid',
|
||||
'isLocationValid',
|
||||
'validationKeys',
|
||||
'isValidToBeConfirmed'
|
||||
|
@@ -19,15 +19,18 @@
|
||||
:options="options"
|
||||
@select="updateOrigin">
|
||||
</VueMultiselect>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="!isOriginValid" class="alert alert-warning to-confirm">
|
||||
{{ $t('origin.not_valid') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueMultiselect from 'vue-multiselect';
|
||||
import { getListOrigins } from '../api';
|
||||
import { mapState } from 'vuex';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'OriginDemand',
|
||||
@@ -41,6 +44,9 @@ export default {
|
||||
...mapState({
|
||||
value: state => state.accompanyingCourse.origin,
|
||||
}),
|
||||
...mapGetters([
|
||||
'isOriginValid'
|
||||
])
|
||||
},
|
||||
mounted() {
|
||||
this.getOptions();
|
||||
|
@@ -5,7 +5,7 @@
|
||||
addId : false,
|
||||
addEntity: false,
|
||||
addLink: false,
|
||||
addHouseholdLink: true,
|
||||
addHouseholdLink: false,
|
||||
addAltNames: true,
|
||||
addAge : true,
|
||||
hLevel : 3,
|
||||
@@ -20,14 +20,15 @@
|
||||
v-if="hasCurrentHouseholdAddress"
|
||||
v-bind:person="participation.person">
|
||||
</button-location>
|
||||
<li v-if="participation.person.current_household_id">
|
||||
<a class="btn btn-sm btn-chill-beige"
|
||||
:href="getCurrentHouseholdUrl"
|
||||
:title="$t('persons_associated.show_household_number', { id: participation.person.current_household_id })">
|
||||
<i class="fa fa-fw fa-home"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="show"></on-the-fly></li>
|
||||
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
|
||||
<!-- <li>
|
||||
<button class="btn btn-delete"
|
||||
:title="$t('action.delete')"
|
||||
@click.prevent="$emit('remove', participation)">
|
||||
</button>
|
||||
</li> -->
|
||||
<li>
|
||||
<button v-if="!participation.endDate"
|
||||
class="btn btn-sm btn-remove"
|
||||
@@ -100,6 +101,9 @@ export default {
|
||||
},
|
||||
getAccompanyingCourseReturnPath() {
|
||||
return `fr/parcours/${this.$store.state.accompanyingCourse.id}/edit#section-10`;
|
||||
},
|
||||
getCurrentHouseholdUrl() {
|
||||
return `/fr/person/household/${this.participation.person.current_household_id}/summary?returnPath=${this.getAccompanyingCourseReturnPath}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@@ -19,16 +19,16 @@
|
||||
@select="updateReferrer">
|
||||
</VueMultiselect>
|
||||
|
||||
|
||||
<template v-if="referrersSuggested.length > 0">
|
||||
<ul>
|
||||
<li v-for="u in referrersSuggested" @click="updateReferrer(u)">
|
||||
<user-render-box-badge :user="u"></user-render-box-badge>
|
||||
</li>
|
||||
<ul class="list-unstyled">
|
||||
<li v-for="u in referrersSuggested" @click="updateReferrer(u)">
|
||||
<span class="badge bg-primary" style="cursor: pointer">
|
||||
<i class="fa fa-plus fa-fw text-success"></i>
|
||||
<user-render-box-badge :user="u"></user-render-box-badge>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@@ -34,6 +34,7 @@ const appMessages = {
|
||||
title: "Origine de la demande",
|
||||
label: "Origine de la demande",
|
||||
placeholder: "Renseignez l'origine de la demande",
|
||||
not_valid: "Indiquez une origine de la demande",
|
||||
},
|
||||
persons_associated: {
|
||||
title: "Usagers concernés",
|
||||
@@ -52,7 +53,7 @@ const appMessages = {
|
||||
show_household_number: "Voir le ménage (n° {id})",
|
||||
show_household: "Voir le ménage",
|
||||
person_without_household_warning: "Certaines usagers n'appartiennent actuellement à aucun ménage. Renseignez leur appartenance dès que possible.",
|
||||
update_household: "Modifier l'appartenance",
|
||||
update_household: "Renseigner l'appartenance",
|
||||
participation_not_valid: "Sélectionnez ou créez au minimum 1 usager",
|
||||
},
|
||||
requestor: {
|
||||
@@ -124,6 +125,7 @@ const appMessages = {
|
||||
participation_not_valid: "sélectionnez au minimum 1 usager",
|
||||
socialIssue_not_valid: "sélectionnez au minimum une problématique sociale",
|
||||
location_not_valid: "indiquez au minimum une localisation temporaire du parcours",
|
||||
origin_not_valid: "indiquez une origine de la demande",
|
||||
set_a_scope: "indiquez au moins un service",
|
||||
sure: "Êtes-vous sûr ?",
|
||||
sure_description: "Une fois le changement confirmé, il ne sera plus possible de le remettre à l'état de brouillon !",
|
||||
|
@@ -52,6 +52,9 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
|
||||
isSocialIssueValid(state) {
|
||||
return state.accompanyingCourse.socialIssues.length > 0;
|
||||
},
|
||||
isOriginValid(state) {
|
||||
return state.accompanyingCourse.origin !== null;
|
||||
},
|
||||
isLocationValid(state) {
|
||||
return state.accompanyingCourse.location !== null;
|
||||
},
|
||||
@@ -64,6 +67,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
|
||||
if (!getters.isParticipationValid) { keys.push('participation'); }
|
||||
if (!getters.isLocationValid) { keys.push('location'); }
|
||||
if (!getters.isSocialIssueValid) { keys.push('socialIssue'); }
|
||||
if (!getters.isOriginValid) { keys.push('origin'); }
|
||||
if (!getters.isScopeValid) { keys.push('scopes'); }
|
||||
//console.log('getter keys', keys);
|
||||
return keys;
|
||||
|
@@ -119,9 +119,9 @@
|
||||
<div id="persons" class="action-row">
|
||||
<h3>{{ $t('persons_involved') }}</h3>
|
||||
|
||||
<ul>
|
||||
<ul class="list-unstyled">
|
||||
<li v-for="p in personsReachables" :key="p.id">
|
||||
<input v-model="personsPicked" :value="p.id" type="checkbox">
|
||||
<input v-model="personsPicked" :value="p.id" type="checkbox" class="me-2">
|
||||
<person-render-box render="badge" :options="{}" :person="p"></person-render-box>
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -96,14 +96,9 @@
|
||||
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
|
||||
</li>
|
||||
|
||||
<li v-if="person.center && options.addCenter">
|
||||
<i class="fa fa-li fa-long-arrow-right"></i>
|
||||
<template v-if="person.center.type !== undefined">
|
||||
{{ person.center.name }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="c in person.center">{{ c.name }}</template>
|
||||
</template>
|
||||
<li v-if="person.centers !== undefined && person.centers.length > 0 && options.addCenter">
|
||||
<i class="fa fa-li fa-long-arrow-right"></i>
|
||||
<template v-for="c in person.centers">{{ c.name }}</template>
|
||||
</li>
|
||||
<li v-else-if="options.addNoData">
|
||||
<i class="fa fa-li fa-long-arrow-right"></i>
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<a class="btn btn-sm btn-update change-icon"
|
||||
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-10' }) }}">
|
||||
<i class="fa fa-fw fa-crosshairs"></i>
|
||||
Corriger
|
||||
{{ 'fix it'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<a class="btn btn-sm btn-update change-icon"
|
||||
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-20' }) }}">
|
||||
<i class="fa fa-fw fa-crosshairs"></i>
|
||||
Corriger
|
||||
{{ 'fix it'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -23,11 +23,28 @@
|
||||
</div>
|
||||
<div id="header-accompanying_course-details" class="header-details">
|
||||
<div class="container-xxl">
|
||||
<div
|
||||
class="row justify-content-md-right">
|
||||
<div class="row">
|
||||
|
||||
{# vue teleport fragment here #}
|
||||
<div class="col-md-10 ps-md-5 ps-xxl-0" id="banner-social-issues"></div>
|
||||
<div class="col-md-12 px-md-5 px-xxl-0">
|
||||
<div id="ACHeaderSlider" class="carousel carousel-dark slide" data-bs-ride="carousel">
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
{# vue teleport fragment here #}
|
||||
<div id="banner-social-issues" class="col-11"></div>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
{# vue teleport fragment here #}
|
||||
<div id="banner-persons-associated" class="col-11"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="carousel-control-prev justify-content-end visually-hidden" type="button" data-bs-target="#ACHeaderSlider" data-bs-slide="prev">
|
||||
<span class="to-social-issues" title="{{ 'see social issues'|trans }}"></span>
|
||||
</button>
|
||||
<button class="carousel-control-next justify-content-end visually-hidden" type="button" data-bs-target="#ACHeaderSlider" data-bs-slide="next">
|
||||
<span class="to-persons-associated" title="{{ 'see persons associated'|trans }}"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -23,32 +23,6 @@
|
||||
{% block content %}
|
||||
<div class="accompanyingcourse-resume row">
|
||||
|
||||
<div class="associated-persons mb-5">
|
||||
{% for h in participationsByHousehold %}
|
||||
{% set householdClass = (h.household is not null) ? 'household-' ~ h.household.id : 'no-household alert alert-warning' %}
|
||||
{% set householdTitle = (h.household is not null) ?
|
||||
'household.Household number'|trans({'household_num': h.household.id }) : 'household.Never in any household'|trans %}
|
||||
<span class="household {{ householdClass }}" title="{{ householdTitle }}">
|
||||
{% if h.household is not null %}
|
||||
<a href="{{ path('chill_person_household_summary', { 'household_id': h.household.id }) }}"
|
||||
title="{{ 'household.Household number'|trans({'household_num': h.household.id }) }}"
|
||||
><i class="fa fa-home fa-fw"></i></a>
|
||||
{% endif %}
|
||||
{% for p in h.members %}
|
||||
|
||||
{# include vue_onthefly component #}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: p.person.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: p.person|chill_entity_render_string
|
||||
} %}
|
||||
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if 'DRAFT' == accompanyingCourse.step %}
|
||||
<div class="col-md-6 warnings mb-5">
|
||||
{% include '@ChillPerson/AccompanyingCourse/_still_draft.html.twig' %}
|
||||
@@ -83,7 +57,7 @@
|
||||
</div>
|
||||
|
||||
<div class="social-actions mb-5">
|
||||
<h2 class="mb-3">{{ 'Last social actions'|trans }}</h2>
|
||||
<h2 class="mb-3 d-none">{{ 'Last social actions'|trans }}</h2>
|
||||
{% include 'ChillPersonBundle:AccompanyingCourseWork:list_recent_by_accompanying_period.html.twig' with {'buttonText': false } %}
|
||||
</div>
|
||||
|
||||
@@ -101,8 +75,7 @@
|
||||
{% set accompanying_course_id = accompanyingCourse.id %}
|
||||
{% endif %}
|
||||
|
||||
<h2 class="mb-3">{{ 'Last activities' |trans }}</h2>
|
||||
|
||||
<h2 class="mb-3 d-none">{{ 'Last activities' |trans }}</h2>
|
||||
{% include 'ChillActivityBundle:Activity:list_recent.html.twig' with { 'context': 'accompanyingCourse', 'no_action': true } %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,29 +1,33 @@
|
||||
{% if works|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}
|
||||
<a class="btn btn-sm btn-create"
|
||||
href="" title="TODO"></a>{# TODO link #}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="accompanying_course_work-list">
|
||||
{% for w in works | slice(0,5) %}
|
||||
<a href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}"></a>
|
||||
|
||||
<h2 class="badge-title">
|
||||
<span class="title_label">{{ 'accompanying_course_work.action'|trans }}</span>
|
||||
<span class="title_action">{{ w.socialAction|chill_entity_render_string }}
|
||||
<span class="title_label">
|
||||
<span>{{ 'accompanying_course_work.action'|trans }}</span>
|
||||
</span>
|
||||
<span class="title_action">
|
||||
{{ w.socialAction|chill_entity_render_string }}
|
||||
|
||||
<ul class="small_in_title">
|
||||
<li>
|
||||
<abbr title="{{ 'accompanying_course_work.start_date'|trans }}">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</abbr>
|
||||
{{ w.startDate|format_date('short') }}
|
||||
<span class="item-key">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</span>
|
||||
<b>{{ w.startDate|format_date('short') }}</b>
|
||||
</li>
|
||||
{% if w.endDate %}
|
||||
<li>
|
||||
<abbr title="{{ 'Last updated by'|trans }}">{{ 'Last updated by'|trans ~ ' : ' }}</abbr>
|
||||
{{ w.updatedBy|chill_entity_render_box }}, {{ w.updatedAt|format_datetime('short', 'short') }}
|
||||
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
|
||||
<b>{{ w.endDate|format_date('short') }}</b>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="metadata text-end" style="font-size: 60%">
|
||||
{{ 'Last updated by'|trans }}
|
||||
<span class="user">{{ w.updatedBy|chill_entity_render_box }}</span>:
|
||||
<span class="date">{{ w.updatedAt|format_datetime('short', 'short') }}</span>
|
||||
</div>
|
||||
|
||||
</span>
|
||||
</h2>
|
||||
{% endfor %}
|
||||
|
@@ -148,10 +148,12 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% if options['addCenter'] and person|chill_resolve_center is not null %}
|
||||
{% if options['addCenter'] and person|chill_resolve_center|length > 0 %}
|
||||
<li>
|
||||
<i class="fa fa-li fa-long-arrow-right"></i>
|
||||
{{ person|chill_resolve_center.name }}
|
||||
{% for c in person|chill_resolve_center %}
|
||||
{{ c.name|upper }}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
@@ -17,18 +17,14 @@
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="text-md-end">
|
||||
{% if person|chill_resolve_center is not null %}
|
||||
{% if person|chill_resolve_center|length > 0 %}
|
||||
<span class="open_sansbold">
|
||||
{{ 'Center'|trans|upper}} :
|
||||
</span>
|
||||
|
||||
{% if person|chill_resolve_center is iterable %}
|
||||
{% for c in person|chill_resolve_center %}
|
||||
{{ c.name|upper }}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{{ person|chill_resolve_center.name|upper }}
|
||||
{% endif %}
|
||||
{% for c in person|chill_resolve_center %}
|
||||
{{ c.name|upper }}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{%- endif -%}
|
||||
</div>
|
||||
|
@@ -1,7 +1,12 @@
|
||||
{% macro button_person(person) %}
|
||||
{% macro button_person_after(person) %}
|
||||
{% set household = person.getCurrentHousehold %}
|
||||
{% if household is not null %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_household_summary', { 'household_id': household.id }) }}" class="btn btn-sm btn-chill-beige"><i class="fa fa-home"></i></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_new', { 'person_id': [ person.id ]}) }}"
|
||||
class="btn btn-sm btn-create change-icon" title="{{ 'Create an accompanying period'|trans }}"><i class="fa fa-random"></i></a>
|
||||
<a href="{{ path('chill_person_accompanying_course_new', { 'person_id': [ person.id ]}) }}" class="btn btn-sm btn-create change-icon" title="{{ 'Create an accompanying period'|trans }}"><i class="fa fa-random"></i></a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -56,7 +61,7 @@
|
||||
'addAltNames': true,
|
||||
'addCenter': true,
|
||||
'address_multiline': false,
|
||||
'customButtons': { 'after': _self.button_person(person) }
|
||||
'customButtons': { 'after': _self.button_person_after(person) }
|
||||
}) }}
|
||||
|
||||
{#- 'acps' is for AcCompanyingPeriodS #}
|
||||
@@ -76,12 +81,20 @@
|
||||
<div class="wl-row separator">
|
||||
<div class="wl-col title">
|
||||
|
||||
<div class="date">
|
||||
{% if acp.requestorPerson == person %}
|
||||
<span class="as-requestor badge bg-info" title="{{ 'Requestor'|trans|e('html_attr') }}">
|
||||
{% if acp.step == 'DRAFT' %}
|
||||
<div class="is-draft">
|
||||
<span class="course-draft badge bg-secondary" title="{{ 'course.draft'|trans }}">{{ 'course.draft'|trans }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if acp.requestorPerson == person %}
|
||||
<div>
|
||||
<span class="as-requestor badge bg-info" title="{{ 'Requestor'|trans|e('html_attr') }}">
|
||||
{{ 'Requestor'|trans({'gender': person.gender}) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="date">
|
||||
{% if app != null %}
|
||||
{{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }}
|
||||
{% endif %}
|
||||
@@ -94,6 +107,11 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="courseid">
|
||||
{{ 'File number'|trans }} {{ acp.id }}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
|
||||
@@ -101,17 +119,76 @@
|
||||
{{ issue|chill_entity_render_box }}
|
||||
{% endfor %}
|
||||
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions record_actions_column">
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': acp.id }) }}"
|
||||
class="btn btn-sm btn-outline-primary" title="{{ 'See accompanying period'|trans }}">
|
||||
<i class="fa fa-random fa-fw"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% if acp.currentParticipations|length > 1 %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<div class="participants">
|
||||
{{ 'Participants'|trans }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% set participating = false %}
|
||||
{% for part in acp.currentParticipations %}
|
||||
{% if part.person.id != person.id %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: part.person.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: part.person|chill_entity_render_string
|
||||
} %}
|
||||
{% else %}
|
||||
{% set participating = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if participating %}
|
||||
{{ 'person.and_himself'|trans({'gender': person.gender}) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if (acp.requestorPerson is not null and acp.requestorPerson.id != person.id) or acp.requestorThirdParty is not null %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<div>
|
||||
{% if acp.requestorPerson is not null %}
|
||||
{{ 'Requestor'|trans({'gender': acp.requestorPerson.gender}) }}
|
||||
{% else %}
|
||||
{{ 'Requestor'|trans({'gender': 'other'})}}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% if acp.requestorThirdParty is not null %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'thirdparty', id: acp.requestorThirdParty.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: acp.requestorThirdParty|chill_entity_render_string
|
||||
} %}
|
||||
{% else %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: acp.requestorPerson.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: acp.requestorPerson|chill_entity_render_string
|
||||
} %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -5,11 +5,15 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
|
||||
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
|
||||
use Chill\PersonBundle\Form\Type\GenderType;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TelType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
@@ -19,23 +23,29 @@ use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterface
|
||||
{
|
||||
protected EngineInterface $templating;
|
||||
protected PaginatorFactory $paginatorFactory;
|
||||
protected PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
private EngineInterface $templating;
|
||||
private PaginatorFactory $paginatorFactory;
|
||||
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
private ExtractDateFromPattern $extractDateFromPattern;
|
||||
private ExtractPhonenumberFromPattern $extractPhonenumberFromPattern;
|
||||
|
||||
public const NAME = "person_regular";
|
||||
|
||||
private const POSSIBLE_KEYS = [
|
||||
'_default', 'firstname', 'lastname', 'birthdate', 'birthdate-before',
|
||||
'birthdate-after', 'gender', 'nationality'
|
||||
'birthdate-after', 'gender', 'nationality', 'phonenumber', 'city'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
EngineInterface $templating,
|
||||
ExtractDateFromPattern $extractDateFromPattern,
|
||||
ExtractPhonenumberFromPattern $extractPhonenumberFromPattern,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository
|
||||
) {
|
||||
$this->templating = $templating;
|
||||
$this->extractDateFromPattern = $extractDateFromPattern;
|
||||
$this->extractPhonenumberFromPattern = $extractPhonenumberFromPattern;
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
}
|
||||
@@ -69,6 +79,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
*/
|
||||
public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html')
|
||||
{
|
||||
$terms = $this->findAdditionnalInDefault($terms);
|
||||
$total = $this->count($terms);
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
@@ -99,6 +110,26 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
}
|
||||
}
|
||||
|
||||
private function findAdditionnalInDefault(array $terms): array
|
||||
{
|
||||
// chaining some extractor
|
||||
$datesResults = $this->extractDateFromPattern->extractDates($terms['_default']);
|
||||
$phoneResults = $this->extractPhonenumberFromPattern->extractPhonenumber($datesResults->getFilteredSubject());
|
||||
$terms['_default'] = $phoneResults->getFilteredSubject();
|
||||
|
||||
if ($datesResults->hasResult() && (!\array_key_exists('birthdate', $terms)
|
||||
|| NULL !== $terms['birthdate'])) {
|
||||
$terms['birthdate'] = $datesResults->getFound()[0]->format('Y-m-d');
|
||||
}
|
||||
|
||||
if ($phoneResults->hasResult() && (!\array_key_exists('phonenumber', $terms)
|
||||
|| NULL !== $terms['phonenumber'])) {
|
||||
$terms['phonenumber'] = $phoneResults->getFound()[0];
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Person[]
|
||||
*/
|
||||
@@ -113,6 +144,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
'birthdate-after' => $birthdateAfter,
|
||||
'gender' => $gender,
|
||||
'nationality' => $countryCode,
|
||||
'phonenumber' => $phonenumber,
|
||||
'city' => $city,
|
||||
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
|
||||
|
||||
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
|
||||
@@ -139,6 +172,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
$birthdateAfter,
|
||||
$gender,
|
||||
$countryCode,
|
||||
$phonenumber,
|
||||
$city
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,7 +188,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
'birthdate-after' => $birthdateAfter,
|
||||
'gender' => $gender,
|
||||
'nationality' => $countryCode,
|
||||
|
||||
'phonenumber' => $phonenumber,
|
||||
'city' => $city,
|
||||
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
|
||||
|
||||
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
|
||||
@@ -177,6 +213,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
$birthdateAfter,
|
||||
$gender,
|
||||
$countryCode,
|
||||
$phonenumber,
|
||||
$city
|
||||
);
|
||||
}
|
||||
|
||||
@@ -207,13 +245,19 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
'label' => 'Birthdate before',
|
||||
'required' => false
|
||||
])
|
||||
->add('gender', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'Man' => Person::MALE_GENDER,
|
||||
'Woman' => Person::FEMALE_GENDER
|
||||
],
|
||||
->add('phonenumber', TelType::class, [
|
||||
'required' => false,
|
||||
'label' => 'Part of the phonenumber'
|
||||
])
|
||||
->add('gender', GenderType::class, [
|
||||
'label' => 'Gender',
|
||||
'required' => false
|
||||
'required' => false,
|
||||
'expanded' => false,
|
||||
'placeholder' => 'All genders'
|
||||
])
|
||||
->add('city', TextType::class, [
|
||||
'required' => false,
|
||||
'label' => 'City or postal code'
|
||||
])
|
||||
;
|
||||
}
|
||||
@@ -224,7 +268,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
|
||||
$string .= empty($data['_default']) ? '' : $data['_default'].' ';
|
||||
|
||||
foreach(['firstname', 'lastname', 'gender'] as $key) {
|
||||
foreach(['firstname', 'lastname', 'gender', 'phonenumber', 'city'] as $key) {
|
||||
$string .= empty($data[$key]) ? '' : $key.':'.
|
||||
// add quote if contains spaces
|
||||
(strpos($data[$key], ' ') !== false ? '"'.$data[$key].'"': $data[$key])
|
||||
@@ -246,7 +290,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach(['firstname', 'lastname', 'gender', '_default'] as $key) {
|
||||
foreach(['firstname', 'lastname', 'gender', '_default', 'phonenumber', 'city'] as $key) {
|
||||
$data[$key] = $terms[$key] ?? null;
|
||||
}
|
||||
|
||||
@@ -275,6 +319,4 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1,133 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2014-2019, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
class PersonSearchByPhone extends AbstractSearch
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PersonRepository
|
||||
*/
|
||||
private $personRepository;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
private $tokenStorage;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
private $helper;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PaginatorFactory
|
||||
*/
|
||||
protected $paginatorFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $activeByDefault;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Templating
|
||||
*/
|
||||
protected $engine;
|
||||
|
||||
const NAME = 'phone';
|
||||
|
||||
public function __construct(
|
||||
PersonRepository $personRepository,
|
||||
TokenStorageInterface $tokenStorage,
|
||||
AuthorizationHelper $helper,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
EngineInterface $engine,
|
||||
$activeByDefault)
|
||||
{
|
||||
$this->personRepository = $personRepository;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->helper = $helper;
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->engine = $engine;
|
||||
$this->activeByDefault = $activeByDefault === 'always';
|
||||
}
|
||||
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 110;
|
||||
}
|
||||
|
||||
public function isActiveByDefault(): bool
|
||||
{
|
||||
return $this->activeByDefault;
|
||||
}
|
||||
|
||||
public function renderResult(array $terms, $start = 0, $limit = 50, $options = array(), $format = 'html')
|
||||
{
|
||||
$phonenumber = $terms['_default'];
|
||||
$centers = $this->helper->getReachableCenters($this->tokenStorage
|
||||
->getToken()->getUser(), new Role(PersonVoter::SEE));
|
||||
$total = $this->personRepository
|
||||
->countByPhone($phonenumber, $centers);
|
||||
$persons = $this->personRepository
|
||||
->findByPhone($phonenumber, $centers, $start, $limit)
|
||||
;
|
||||
$paginator = $this->paginatorFactory
|
||||
->create($total);
|
||||
|
||||
return $this->engine->render('ChillPersonBundle:Person:list_by_phonenumber.html.twig',
|
||||
array(
|
||||
'persons' => $persons,
|
||||
'pattern' => $this->recomposePattern($terms, array(), $terms['_domain'] ?? self::NAME),
|
||||
'phonenumber' => $phonenumber,
|
||||
'total' => $total,
|
||||
'start' => $start,
|
||||
'search_name' => self::NAME,
|
||||
'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION],
|
||||
'paginator' => $paginator
|
||||
));
|
||||
}
|
||||
|
||||
public function supports($domain, $format): bool
|
||||
{
|
||||
return $domain === 'phone' && $format === 'html';
|
||||
}
|
||||
}
|
@@ -2,12 +2,13 @@
|
||||
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
|
||||
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Chill\MainBundle\Search\SearchApiInterface;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class SearchPersonApiProvider implements SearchApiInterface
|
||||
@@ -15,59 +16,47 @@ class SearchPersonApiProvider implements SearchApiInterface
|
||||
private PersonRepository $personRepository;
|
||||
private Security $security;
|
||||
private AuthorizationHelperInterface $authorizationHelper;
|
||||
private ExtractDateFromPattern $extractDateFromPattern;
|
||||
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
private ExtractPhonenumberFromPattern $extractPhonenumberFromPattern;
|
||||
|
||||
public function __construct(PersonRepository $personRepository, Security $security, AuthorizationHelperInterface $authorizationHelper)
|
||||
{
|
||||
public function __construct(
|
||||
PersonRepository $personRepository,
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository,
|
||||
Security $security,
|
||||
AuthorizationHelperInterface $authorizationHelper,
|
||||
ExtractDateFromPattern $extractDateFromPattern,
|
||||
ExtractPhonenumberFromPattern $extractPhonenumberFromPattern
|
||||
) {
|
||||
$this->personRepository = $personRepository;
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
$this->security = $security;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->extractDateFromPattern = $extractDateFromPattern;
|
||||
$this->extractPhonenumberFromPattern = $extractPhonenumberFromPattern;
|
||||
}
|
||||
|
||||
public function provideQuery(string $pattern, array $parameters): SearchApiQuery
|
||||
{
|
||||
return $this->addAuthorizations($this->buildBaseQuery($pattern, $parameters));
|
||||
}
|
||||
$datesResult = $this->extractDateFromPattern->extractDates($pattern);
|
||||
$phoneResult = $this->extractPhonenumberFromPattern->extractPhonenumber($datesResult->getFilteredSubject());
|
||||
$filtered = $phoneResult->getFilteredSubject();
|
||||
|
||||
public function buildBaseQuery(string $pattern, array $parameters): SearchApiQuery
|
||||
{
|
||||
$query = new SearchApiQuery();
|
||||
$query
|
||||
return $this->personACLAwareRepository->buildAuthorizedQuery(
|
||||
$filtered,
|
||||
null,
|
||||
null,
|
||||
count($datesResult->getFound()) > 0 ? $datesResult->getFound()[0] : null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
count($phoneResult->getFound()) > 0 ? $phoneResult->getFound()[0] : null
|
||||
)
|
||||
->setSelectKey("person")
|
||||
->setSelectJsonbMetadata("jsonb_build_object('id', person.id)")
|
||||
->setSelectPertinence("".
|
||||
"STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ".
|
||||
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + ".
|
||||
"(EXISTS (SELECT 1 FROM unnest(string_to_array(fullnamecanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int"
|
||||
, [ $pattern, $pattern, $pattern ])
|
||||
->setFromClause("chill_person_person AS person")
|
||||
->setWhereClauses("LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
|
||||
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ", [ $pattern, $pattern ])
|
||||
;
|
||||
|
||||
return $query;
|
||||
->setSelectJsonbMetadata("jsonb_build_object('id', person.id)");
|
||||
}
|
||||
|
||||
private function addAuthorizations(SearchApiQuery $query): SearchApiQuery
|
||||
{
|
||||
$authorizedCenters = $this->authorizationHelper
|
||||
->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
|
||||
|
||||
if ([] === $authorizedCenters) {
|
||||
return $query->andWhereClause("FALSE = TRUE", []);
|
||||
}
|
||||
|
||||
return $query
|
||||
->andWhereClause(
|
||||
strtr(
|
||||
"person.center_id IN ({{ center_ids }})",
|
||||
[
|
||||
'{{ center_ids }}' => \implode(', ',
|
||||
\array_fill(0, count($authorizedCenters), '?')),
|
||||
]
|
||||
),
|
||||
\array_map(function(Center $c) {return $c->getId();}, $authorizedCenters)
|
||||
);
|
||||
}
|
||||
|
||||
public function supportsTypes(string $pattern, array $types, array $parameters): bool
|
||||
{
|
||||
|
@@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
class SimilarityPersonSearch extends AbstractSearch
|
||||
{
|
||||
protected PaginatorFactory $paginatorFactory;
|
||||
|
||||
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
|
||||
private EngineInterface $templating;
|
||||
|
||||
const NAME = "person_similarity";
|
||||
|
||||
public function __construct(
|
||||
PaginatorFactory $paginatorFactory,
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository,
|
||||
EngineInterface $templating
|
||||
) {
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
$this->templating = $templating;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-PHPdoc)
|
||||
* @see \Chill\MainBundle\Search\SearchInterface::getOrder()
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return 200;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-PHPdoc)
|
||||
* @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault()
|
||||
*/
|
||||
public function isActiveByDefault()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supports($domain, $format)
|
||||
{
|
||||
return 'person' === $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $terms
|
||||
* @param int $start
|
||||
* @param int $limit
|
||||
* @param array $options
|
||||
* @param string $format
|
||||
*/
|
||||
public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html')
|
||||
{
|
||||
$total = $this->count($terms);
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
if ($format === 'html') {
|
||||
if ($total !== 0) {
|
||||
return $this->templating->render('ChillPersonBundle:Person:list.html.twig',
|
||||
array(
|
||||
'persons' => $this->search($terms, $start, $limit, $options),
|
||||
'pattern' => $this->recomposePattern($terms, array('nationality',
|
||||
'firstname', 'lastname', 'birthdate', 'gender',
|
||||
'birthdate-before','birthdate-after'), $terms['_domain']),
|
||||
'total' => $total,
|
||||
'start' => $start,
|
||||
'search_name' => self::NAME,
|
||||
'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION],
|
||||
'paginator' => $paginator,
|
||||
'title' => "Similar persons"
|
||||
));
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} elseif ($format === 'json') {
|
||||
return [
|
||||
'results' => $this->search($terms, $start, $limit, \array_merge($options, [ 'simplify' => true ])),
|
||||
'pagination' => [
|
||||
'more' => $paginator->hasNextPage()
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $pattern
|
||||
* @param int $start
|
||||
* @param int $limit
|
||||
* @param array $options
|
||||
* @return Person[]
|
||||
*/
|
||||
protected function search(array $terms, $start, $limit, array $options = array())
|
||||
{
|
||||
return $this->personACLAwareRepository
|
||||
->findBySimilaritySearch($terms['_default'], $start, $limit, $options['simplify'] ?? false);
|
||||
}
|
||||
|
||||
protected function count(array $terms)
|
||||
{
|
||||
return $this->personACLAwareRepository->countBySimilaritySearch($terms['_default']);
|
||||
}
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\PersonAltName;
|
||||
use Chill\PersonBundle\Templating\Entity\PersonRender;
|
||||
use Symfony\Component\Serializer\Exception\CircularReferenceException;
|
||||
use Symfony\Component\Serializer\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Exception\LogicException;
|
||||
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class PersonDocGenNormalizer implements
|
||||
ContextAwareNormalizerInterface,
|
||||
NormalizerAwareInterface
|
||||
{
|
||||
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
private PersonRender $personRender;
|
||||
private TranslatorInterface $translator;
|
||||
private TranslatableStringHelper $translatableStringHelper;
|
||||
|
||||
/**
|
||||
* @param PersonRender $personRender
|
||||
* @param TranslatorInterface $translator
|
||||
* @param TranslatableStringHelper $translatableStringHelper
|
||||
*/
|
||||
public function __construct(
|
||||
PersonRender $personRender,
|
||||
TranslatorInterface $translator,
|
||||
TranslatableStringHelper $translatableStringHelper
|
||||
) {
|
||||
$this->personRender = $personRender;
|
||||
$this->translator = $translator;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function normalize($person, string $format = null, array $context = [])
|
||||
{
|
||||
/** @var Person $person */
|
||||
|
||||
$dateContext = $context;
|
||||
$dateContext['docgen:expects'] = \DateTimeInterface::class;
|
||||
|
||||
if (null === $person) {
|
||||
return $this->normalizeNullValue($format, $context);
|
||||
}
|
||||
|
||||
return [
|
||||
'firstname' => $person->getFirstName(),
|
||||
'lastname' => $person->getLastName(),
|
||||
'altNames' => \implode(
|
||||
', ',
|
||||
\array_map(
|
||||
function (PersonAltName $altName) {
|
||||
return $altName->getLabel();
|
||||
},
|
||||
$person->getAltNames()->toArray()
|
||||
)
|
||||
),
|
||||
'text' => $this->personRender->renderString($person, []),
|
||||
'birthdate' => $this->normalizer->normalize($person->getBirthdate(), $format, $dateContext),
|
||||
'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $dateContext),
|
||||
'gender' => $this->translator->trans($person->getGender()),
|
||||
'maritalStatus' => null !== ($ms = $person->getMaritalStatus()) ? $this->translatableStringHelper->localize($ms->getName()) : '',
|
||||
'maritalStatusDate' => $this->normalizer->normalize($person->getMaritalStatusDate(), $format, $dateContext),
|
||||
'email' => $person->getEmail(),
|
||||
'firstPhoneNumber' => $person->getPhonenumber() ?? $person->getMobilenumber(),
|
||||
'fixPhoneNumber' => $person->getPhonenumber(),
|
||||
'mobilePhoneNumber' => $person->getMobilenumber(),
|
||||
'nationality' => null !== ($c = $person->getNationality()) ? $this->translatableStringHelper->localize($c->getName()) : '',
|
||||
'placeOfBirth' => $person->getPlaceOfBirth(),
|
||||
'memo' => $person->getMemo(),
|
||||
'numberOfChildren' => (string) $person->getNumberOfChildren(),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeNullValue(string $format, array $context)
|
||||
{
|
||||
$normalizer = new NormalizeNullValueHelper($this->normalizer);
|
||||
|
||||
$attributes = [
|
||||
'firstname', 'lastname', 'altNames', 'text',
|
||||
'birthdate' => \DateTimeInterface::class,
|
||||
'deathdate' => \DateTimeInterface::class,
|
||||
'gender', 'maritalStatus',
|
||||
'maritalStatusDate' => \DateTimeInterface::class,
|
||||
'email', 'firstPhoneNumber', 'fixPhoneNumber', 'mobilePhoneNumber', 'nationality',
|
||||
'placeOfBirth', 'memo', 'numberOfChildren'
|
||||
];
|
||||
|
||||
return $normalizer->normalize($attributes, $format, $context);
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, string $format = null, array $context = [])
|
||||
{
|
||||
if ($format !== 'docgen') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return
|
||||
$data instanceof Person
|
||||
|| (
|
||||
\array_key_exists('docgen:expects', $context)
|
||||
&& $context['docgen:expects'] === Person::class
|
||||
);
|
||||
}
|
||||
}
|
@@ -20,6 +20,7 @@ namespace Chill\PersonBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
|
||||
@@ -39,7 +40,7 @@ use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
|
||||
* Serialize a Person entity
|
||||
*
|
||||
*/
|
||||
class PersonNormalizer implements
|
||||
class PersonJsonNormalizer implements
|
||||
NormalizerInterface,
|
||||
NormalizerAwareInterface,
|
||||
DenormalizerInterface,
|
||||
@@ -49,7 +50,7 @@ class PersonNormalizer implements
|
||||
|
||||
private PersonRepository $repository;
|
||||
|
||||
private CenterResolverDispatcher $centerResolverDispatcher;
|
||||
private CenterResolverManagerInterface $centerResolverManager;
|
||||
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
@@ -60,11 +61,11 @@ class PersonNormalizer implements
|
||||
public function __construct(
|
||||
ChillEntityRenderExtension $render,
|
||||
PersonRepository $repository,
|
||||
CenterResolverDispatcher $centerResolverDispatcher
|
||||
CenterResolverManagerInterface $centerResolverManager
|
||||
) {
|
||||
$this->render = $render;
|
||||
$this->repository = $repository;
|
||||
$this->centerResolverDispatcher = $centerResolverDispatcher;
|
||||
$this->centerResolverManager = $centerResolverManager;
|
||||
}
|
||||
|
||||
public function normalize($person, string $format = null, array $context = [])
|
||||
@@ -79,15 +80,15 @@ class PersonNormalizer implements
|
||||
'text' => $this->render->renderString($person),
|
||||
'firstName' => $person->getFirstName(),
|
||||
'lastName' => $person->getLastName(),
|
||||
'birthdate' => $this->normalizer->normalize($person->getBirthdate()),
|
||||
'deathdate' => $this->normalizer->normalize($person->getDeathdate()),
|
||||
'center' => $this->normalizer->normalize($this->centerResolverDispatcher->resolveCenter($person)),
|
||||
'birthdate' => $this->normalizer->normalize($person->getBirthdate(), $format, $context),
|
||||
'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $context),
|
||||
'centers' => $this->normalizer->normalize($this->centerResolverManager->resolveCenters($person), $format, $context),
|
||||
'phonenumber' => $person->getPhonenumber(),
|
||||
'mobilenumber' => $person->getMobilenumber(),
|
||||
'altNames' => $this->normalizeAltNames($person->getAltNames()),
|
||||
'gender' => $person->getGender(),
|
||||
'current_household_address' => $this->normalizer->normalize($person->getCurrentHouseholdAddress()),
|
||||
'current_household_id' => $household ? $this->normalizer->normalize($household->getId()) : null,
|
||||
'current_household_address' => $this->normalizer->normalize($person->getCurrentHouseholdAddress(), $format, $context),
|
||||
'current_household_id' => $household ? $this->normalizer->normalize($household->getId(), $format, $context) : null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -105,7 +106,7 @@ class PersonNormalizer implements
|
||||
|
||||
public function supportsNormalization($data, string $format = null): bool
|
||||
{
|
||||
return $data instanceof Person;
|
||||
return $data instanceof Person && $format === 'json';
|
||||
}
|
||||
|
||||
public function denormalize($data, string $type, string $format = null, array $context = [])
|
||||
@@ -128,45 +129,48 @@ class PersonNormalizer implements
|
||||
$person = new Person();
|
||||
}
|
||||
|
||||
$properties = ['firstName', 'lastName', 'phonenumber', 'mobilenumber', 'gender'];
|
||||
foreach (['firstName', 'lastName', 'phonenumber', 'mobilenumber', 'gender',
|
||||
'birthdate', 'deathdate', 'center']
|
||||
as $item) {
|
||||
|
||||
$properties = array_filter(
|
||||
$properties,
|
||||
static fn (string $property): bool => array_key_exists($property, $data)
|
||||
);
|
||||
|
||||
foreach ($properties as $item) {
|
||||
$callable = [$person, sprintf('set%s', ucfirst($item))];
|
||||
|
||||
if (is_callable($callable)) {
|
||||
$closure = \Closure::fromCallable($callable);
|
||||
|
||||
$closure($data[$item]);
|
||||
if (!\array_key_exists($item, $data)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$propertyToClassMapping = [
|
||||
'birthdate' => \DateTime::class,
|
||||
'deathdate' => \DateTime::class,
|
||||
'center' => Center::class,
|
||||
];
|
||||
|
||||
$propertyToClassMapping = array_filter(
|
||||
$propertyToClassMapping,
|
||||
static fn (string $item): bool => array_key_exists($item, $data)
|
||||
);
|
||||
|
||||
foreach ($propertyToClassMapping as $item => $class) {
|
||||
$object = $this->denormalizer->denormalize($data[$item], $class, $format, $context);
|
||||
|
||||
if ($object instanceof $class) {
|
||||
$callable = [$object, sprintf('set%s', ucfirst($item))];
|
||||
|
||||
if (is_callable($callable)) {
|
||||
$closure = \Closure::fromCallable($callable);
|
||||
|
||||
$closure($object);
|
||||
}
|
||||
switch ($item) {
|
||||
case 'firstName':
|
||||
$person->setFirstName($data[$item]);
|
||||
break;
|
||||
case 'lastName':
|
||||
$person->setLastName($data[$item]);
|
||||
break;
|
||||
case 'phonenumber':
|
||||
$person->setPhonenumber($data[$item]);
|
||||
break;
|
||||
case 'mobilenumber':
|
||||
$person->setMobilenumber($data[$item]);
|
||||
break;
|
||||
case 'gender':
|
||||
$person->setGender($data[$item]);
|
||||
break;
|
||||
case 'birthdate':
|
||||
$object = $this->denormalizer->denormalize($data[$item], \DateTime::class, $format, $context);
|
||||
if ($object instanceof \DateTime) {
|
||||
$person->setBirthdate($object);
|
||||
}
|
||||
break;
|
||||
case 'deathdate':
|
||||
$object = $this->denormalizer->denormalize($data[$item], \DateTime::class, $format, $context);
|
||||
if ($object instanceof \DateTime) {
|
||||
$person->setDeathdate($object);
|
||||
}
|
||||
break;
|
||||
case 'center':
|
||||
$object = $this->denormalizer->denormalize($data[$item], Center::class, $format, $context);
|
||||
$person->setCenter($object);
|
||||
break;
|
||||
default:
|
||||
throw new \LogicException("item not defined: $item");
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Serializer\Normalizer;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\PersonAltName;
|
||||
use Chill\PersonBundle\Serializer\Normalizer\PersonDocGenNormalizer;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class PersonDocGenNormalizerTest extends KernelTestCase
|
||||
{
|
||||
private NormalizerInterface $normalizer;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->normalizer = self::$container->get(NormalizerInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider generateData
|
||||
*/
|
||||
public function testNormalize(?Person $person, $expected, $msg)
|
||||
{
|
||||
$normalized = $this->normalizer->normalize($person, 'docgen', ['docgen:expects' => Person::class]);
|
||||
|
||||
$this->assertEquals($expected, $normalized, $msg);
|
||||
}
|
||||
|
||||
public function generateData()
|
||||
{
|
||||
$person = new Person();
|
||||
$person
|
||||
->setFirstName('Renaud')
|
||||
->setLastName('Mégane')
|
||||
;
|
||||
|
||||
$expected = \array_merge(
|
||||
self::BLANK, ['firstname' => 'Renaud', 'lastname' => 'Mégane',
|
||||
'text' => 'Renaud Mégane']
|
||||
);
|
||||
|
||||
yield [$person, $expected, 'partial normalization for a person'];
|
||||
|
||||
yield [null, self::BLANK, 'normalization for a null person'];
|
||||
}
|
||||
|
||||
|
||||
private const BLANK = [
|
||||
'firstname' => '',
|
||||
'lastname' => '',
|
||||
'altNames' => '',
|
||||
'text' => '',
|
||||
'birthdate' => ['short' => '', 'long' => ''],
|
||||
'deathdate' => ['short' => '', 'long' => ''],
|
||||
'gender' => '',
|
||||
'maritalStatus' => '',
|
||||
'maritalStatusDate' => ['short' => '', 'long' => ''],
|
||||
'email' => '',
|
||||
'firstPhoneNumber' => '',
|
||||
'fixPhoneNumber' => '',
|
||||
'mobilePhoneNumber' => '',
|
||||
'nationality' => '',
|
||||
'placeOfBirth' => '',
|
||||
'memo' => '',
|
||||
'numberOfChildren' => ''
|
||||
];
|
||||
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Serializer\Normalizer;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonNormalizer;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class PersonJsonNormalizerTest extends KernelTestCase
|
||||
{
|
||||
private NormalizerInterface $normalizer;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->normalizer = self::$container->get(NormalizerInterface::class);
|
||||
}
|
||||
|
||||
public function testNormalization()
|
||||
{
|
||||
$person = new Person();
|
||||
$result = $this->normalizer->normalize($person, 'json', [AbstractNormalizer::GROUPS => [ 'read' ]]);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
}
|
||||
}
|
@@ -4,11 +4,6 @@ services:
|
||||
tags:
|
||||
- { name: chill.search, alias: 'person_regular' }
|
||||
|
||||
Chill\PersonBundle\Search\SimilarityPersonSearch:
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: chill.search, alias: 'person_similarity' }
|
||||
|
||||
Chill\PersonBundle\Search\SimilarPersonMatcher:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
@@ -1,11 +0,0 @@
|
||||
services:
|
||||
Chill\PersonBundle\Search\PersonSearchByPhone:
|
||||
arguments:
|
||||
- '@Chill\PersonBundle\Repository\PersonRepository'
|
||||
- '@security.token_storage'
|
||||
- '@chill.main.security.authorization.helper'
|
||||
- '@chill_main.paginator_factory'
|
||||
- '@Symfony\Component\Templating\EngineInterface'
|
||||
- '%chill_person.search.search_by_phone%'
|
||||
tags:
|
||||
- { name: chill.search, alias: 'person_by_phone' }
|
@@ -4,6 +4,7 @@ services:
|
||||
|
||||
Chill\PersonBundle\Serializer\Normalizer\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../../Serializer/Normalizer'
|
||||
tags:
|
||||
- { name: 'serializer.normalizer', priority: 64 }
|
||||
|
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Person;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* drop not null in person phonenumber
|
||||
*/
|
||||
final class Version20211112170027 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Drop not null in person table: set default empty value';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('UPDATE chill_person_person SET mobilenumber = \'\' WHERE mobilenumber IS NULL');
|
||||
$this->addSql('UPDATE chill_person_person SET phonenumber = \'\' WHERE phonenumber IS NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber SET DEFAULT \'\'');
|
||||
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber SET DEFAULT \'\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber DROP NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber DROP NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber SET DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber SET DEFAULT NULL');
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Person;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211119211101 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'add indexes for searching by birthdate';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE INDEX person_birthdate ON chill_person_person (birthdate)');
|
||||
$this->addSql('CREATE INDEX phonenumber ON chill_person_phone USING GIST (phonenumber gist_trgm_ops)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX person_birthdate');
|
||||
$this->addSql('DROP INDEX phonenumber');
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user