Merge remote-tracking branch 'origin/master' into issue561_avoid_search_address_with_undefined_city

This commit is contained in:
Julien Fastré 2022-04-27 15:30:12 +02:00
commit ea6a9b3e2d
22 changed files with 191 additions and 97 deletions

View File

@ -12,10 +12,8 @@ and this project adheres to
<!-- write down unreleased development here -->
* [main] avoid address reference search on undefined post code (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/561)
* [person] prevent duplicate relationship in filiation/household graph (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/560)
* [Documents] Validate storedObject and allow for null data (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/565)
* [Activity form] invert 'incoming' and 'receiving' in Activity form
* [Activity form] keep the same order for 'attendee' field in new and edit form
* [list with period] use "sameas" test operator to introduce requestor in list
## Test releases

View File

@ -5,11 +5,6 @@ parameters:
count: 1
path: src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php
-
message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\Entity\\\\Person\\:\\:\\$currentHouseholdParticipationAt\\.$#"
count: 3
path: src/Bundle/ChillPersonBundle/Entity/Person.php
-
message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\Entity\\\\Household\\\\PersonHouseholdAddress\\:\\:\\$relation\\.$#"
count: 1

View File

@ -3,6 +3,7 @@ parameters:
paths:
- src/
excludePaths:
- .php_cs*
- docs/
- src/Bundle/*/Tests/*
- src/Bundle/*/tests/*

View File

@ -16,9 +16,9 @@ use Chill\MainBundle\Serializer\Model\Collection;
use Exception;
use LogicException;
use RuntimeException;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
@ -55,7 +55,7 @@ class ApiController extends AbstractCRUDController
return $this->entityDelete('_entity', $request, $id, $_format);
default:
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException('This method is not implemented');
throw new BadRequestHttpException('This method is not implemented');
}
}
@ -120,7 +120,7 @@ class ApiController extends AbstractCRUDController
return $this->entityPostAction('_entity', $request, $_format);
default:
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException('This method is not implemented');
throw new BadRequestHttpException('This method is not implemented');
}
}
@ -160,7 +160,7 @@ class ApiController extends AbstractCRUDController
try {
$entity = $this->deserialize($action, $request, $_format, $entity);
} catch (NotEncodableValueException $e) {
throw new BadRequestException('invalid json', 400, $e);
throw new BadRequestHttpException('invalid json', 400, $e);
}
$errors = $this->validate($action, $request, $_format, $entity);
@ -273,7 +273,7 @@ class ApiController extends AbstractCRUDController
try {
$postedData = $this->getSerializer()->deserialize($request->getContent(), $postedDataType, $_format, $postedDataContext);
} catch (\Symfony\Component\Serializer\Exception\UnexpectedValueException $e) {
throw new BadRequestException(sprintf('Unable to deserialize posted ' .
throw new BadRequestHttpException(sprintf('Unable to deserialize posted ' .
'data: %s', $e->getMessage()), 0, $e);
}
@ -290,7 +290,7 @@ class ApiController extends AbstractCRUDController
break;
default:
throw new BadRequestException('this method is not supported');
throw new BadRequestHttpException('this method is not supported');
}
$errors = $this->validate($action, $request, $_format, $entity, [$postedData]);
@ -408,7 +408,7 @@ class ApiController extends AbstractCRUDController
return $this->json($entity, Response::HTTP_OK, [], $context);
}
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException('This format is not implemented');
throw new BadRequestHttpException('This format is not implemented');
}
protected function entityPostAction($action, Request $request, string $_format): Response
@ -418,7 +418,7 @@ class ApiController extends AbstractCRUDController
try {
$entity = $this->deserialize($action, $request, $_format, $entity);
} catch (NotEncodableValueException $e) {
throw new BadRequestException('invalid json', 400, $e);
throw new BadRequestHttpException('invalid json', 400, $e);
}
$errors = $this->validate($action, $request, $_format, $entity);

View File

@ -25,6 +25,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@ -1140,6 +1141,6 @@ class CRUDController extends AbstractController
return $this->json($entity, Response::HTTP_OK, [], $context);
}
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException('This format is not implemented');
throw new BadRequestHttpException('This format is not implemented');
}
}

View File

@ -22,7 +22,6 @@ use Chill\MainBundle\Search\UnknowSearchNameException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@ -216,7 +215,7 @@ class SearchController extends AbstractController
$types = $request->query->get('type', []);
if (count($types) === 0) {
throw new BadRequestException('The request must contains at '
throw new BadRequestHttpException('The request must contains at '
. ' one type');
}

View File

@ -25,7 +25,6 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@ -144,7 +143,7 @@ class WorkflowController extends AbstractController
public function getAccessByAccessKey(EntityWorkflowStep $entityWorkflowStep, Request $request): Response
{
if (null === $accessKey = $request->query->get('accessKey', null)) {
throw new BadRequestException('accessKey is missing');
throw new BadRequestHttpException('accessKey is missing');
}
if (!$this->getUser() instanceof User) {

View File

@ -33,10 +33,10 @@ use DateInterval;
use DateTimeImmutable;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Serializer\Exception\RuntimeException;
@ -185,7 +185,7 @@ final class AccompanyingCourseApiController extends ApiController
->deserialize($request->getContent(), Person::class, $_format, []);
if (null === $person) {
throw new BadRequestException('person id not found');
throw new BadRequestHttpException('person id not found');
}
// TODO add acl
@ -204,7 +204,7 @@ final class AccompanyingCourseApiController extends ApiController
break;
default:
throw new BadRequestException('This method is not supported');
throw new BadRequestHttpException('This method is not supported');
}
$errors = $this->validator->validate($accompanyingPeriod);
@ -247,12 +247,12 @@ final class AccompanyingCourseApiController extends ApiController
}
if (null === $requestor) {
throw new BadRequestException('Could not find any person or thirdparty', 0, null);
throw new BadRequestHttpException('Could not find any person or thirdparty', 0, null);
}
$accompanyingPeriod->setRequestor($requestor);
} else {
throw new BadRequestException('method not supported');
throw new BadRequestHttpException('method not supported');
}
$errors = $this->validator->validate($accompanyingPeriod);

View File

@ -21,9 +21,9 @@ use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
@ -255,7 +255,7 @@ class AccompanyingCourseController extends Controller
$personIds = $request->query->get('person_id');
if (false === is_array($personIds)) {
throw new BadRequestException('person_id parameter should be an array');
throw new BadRequestHttpException('person_id parameter should be an array');
}
foreach ($personIds as $personId) {

View File

@ -21,8 +21,8 @@ use Chill\PersonBundle\Security\Authorization\HouseholdVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
@ -202,7 +202,7 @@ class HouseholdController extends AbstractController
$this->denyAccessUnlessGranted(HouseholdVoter::EDIT, $household);
if (!$request->query->has('address_id')) {
throw new BadRequestException('parameter address_id is missing');
throw new BadRequestHttpException('parameter address_id is missing');
}
$address_id = $request->query->getInt('address_id');
@ -218,7 +218,7 @@ class HouseholdController extends AbstractController
}
if (null === $address) {
throw new BadRequestException('The edited address does not belongs to the household');
throw new BadRequestHttpException('The edited address does not belongs to the household');
}
$form = $this->createForm(AddressDateType::class, $address, []);

View File

@ -20,9 +20,9 @@ use Chill\PersonBundle\Form\HouseholdMemberType;
use Chill\PersonBundle\Household\MembersEditor;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Exception;
@ -105,7 +105,7 @@ class HouseholdMemberController extends ApiController
$ids = $request->query->get('persons', []);
if (0 === count($ids)) {
throw new BadRequestException('parameters persons in query ' .
throw new BadRequestHttpException('parameters persons in query ' .
'is not an array or empty');
}
@ -121,7 +121,8 @@ class HouseholdMemberController extends ApiController
}
}
if ($householdId = $request->query->get('household', false)) {
if ($request->query->has('household')) {
$householdId = $request->query->get('household', false);
$household = $em->getRepository(Household::class)
->find($householdId);
$allowHouseholdCreate = false;
@ -189,7 +190,7 @@ class HouseholdMemberController extends ApiController
['groups' => ['read']]
);
} catch (Exception\InvalidArgumentException|Exception\UnexpectedValueException $e) {
throw new BadRequestException("Deserialization error: {$e->getMessage()}", 45896, $e);
throw new BadRequestHttpException("Deserialization error: {$e->getMessage()}", 45896, $e);
}
// TODO ACL

View File

@ -240,6 +240,11 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
*/
private array $currentHouseholdAt = [];
/**
* Cache for the computation of current household participation.
*/
private array $currentHouseholdParticipationAt = [];
/**
* The current person address.
*

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Validator\Constraints\Relationship\RelationshipNoDuplicate;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
@ -31,6 +32,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* @DiscriminatorMap(typeProperty="type", mapping={
* "relationship": Relationship::class
* })
* @RelationshipNoDuplicate
*/
class Relationship implements TrackCreationInterface, TrackUpdateInterface
{

View File

@ -472,15 +472,25 @@ export default {
case 'create':
return postRelationship(this.modal.data)
.then(relationship => new Promise(resolve => {
//console.log('post relationship response', relationship)
this.$store.dispatch('addLinkFromRelationship', relationship)
this.modal.showModal = false
this.resetForm()
this.forceUpdateComponent()
resolve()
}))
.catch()
.then(relationship => new Promise(resolve => {
//console.log('post relationship response', relationship)
this.$store.dispatch('addLinkFromRelationship', relationship)
this.modal.showModal = false
this.resetForm()
this.forceUpdateComponent()
resolve()
}))
.catch( error => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
console.log(v)
}
} else {
this.$toast.open({message: 'An error occurred'});
}
}
)
case 'edit':
return patchRelationship(this.modal.data)

View File

@ -1,50 +1,5 @@
import { splitId } from './vis-network'
/**
* @function makeFetch
* @param method
* @param url
* @param body
* @returns {Promise<Response>}
*/
const makeFetch = (method, url, body) => {
return fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: (body !== null) ? JSON.stringify(body) : null
})
.then(response => {
if (response.ok) {
return response.json();
}
if (response.status === 422) {
return response.json().then(violations => {
throw ValidationException(violations)
});
}
throw {
msg: 'Error while updating AccompanyingPeriod Course.',
sta: response.status,
txt: response.statusText,
err: new Error(),
body: response.body
};
});
}
/**
* @param violations
* @constructor
*/
const ValidationException = (violations) => {
this.violations = violations
this.name = 'ValidationException'
}
import { splitId } from './vis-network';
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.js';
/**
* @function getFetch
@ -136,7 +91,7 @@ const getRelationsList = () => {
* @returns {Promise<Response>}
*/
const postRelationship = (relationship) => {
//console.log(relationship)
//console.log(relationship);
return postFetch(
`/api/1.0/relations/relationship.json`,
{

View File

@ -3,8 +3,10 @@ import { store } from "./store.js"
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
import { visMessages } from './i18n'
import App from './App.vue'
import VueToast from 'vue-toast-notification';
import 'vue-toast-notification/dist/theme-sugar.css';
import './vis-network'
import './vis-network';
const i18n = _createI18n(visMessages)
const container = document.getElementById('relationship-graph')
@ -25,5 +27,11 @@ const app = createApp({
})
.use(store)
.use(i18n)
.use(VueToast, {
position: "bottom-right",
type: "error",
duration: 5000,
dismissible: true
})
.component('app', App)
.mount('#relationship-graph')

View File

@ -0,0 +1,27 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Validator\Constraints\Relationship;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class RelationshipNoDuplicate extends Constraint
{
public $message = 'relationship.duplicate';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Validator\Constraints\Relationship;
use Chill\PersonBundle\Repository\Relationships\RelationshipRepository;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RelationshipNoDuplicateValidator extends ConstraintValidator
{
private RelationshipRepository $relationshipRepository;
public function __construct(RelationshipRepository $relationshipRepository)
{
$this->relationshipRepository = $relationshipRepository;
}
public function validate($relationship, Constraint $constraint)
{
if (!$constraint instanceof RelationshipNoDuplicate) {
throw new UnexpectedTypeException($constraint, RelationshipNoDuplicate::class);
}
$fromPerson = $relationship->getFromPerson();
$toPerson = $relationship->getToPerson();
$relationships = $this->relationshipRepository->findBy([
'fromPerson' => [$fromPerson, $toPerson],
'toPerson' => [$fromPerson, $toPerson],
]);
foreach ($relationships as $r) {
if (
$r->getFromPerson() === $fromPerson
|| $r->getFromPerson() === $toPerson
|| $r->getToPerson() === $fromPerson
|| $r->getToPerson() === $toPerson
) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add constraint with a index on chill_person_relationships.
*/
final class Version20220425000000 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX IDX_RELATIONSHIPS000');
}
public function getDescription(): string
{
return 'Add constraint with a index on chill_person_relationships.';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE UNIQUE INDEX IDX_RELATIONSHIPS000 ON chill_person_relationships (least(fromperson_id, toperson_id), greatest(fromperson_id, toperson_id))');
}
}

View File

@ -62,3 +62,7 @@ You cannot associate a resource with the same person: Vous ne pouvez pas ajouter
#location
The period must remain located: 'Un parcours doit être localisé'
The person where the course is located must be associated to the course. Change course's location before removing the person.: "Le parcours est localisé auprès cet usager. Veuillez changer la localisation du parcours avant de suprimer l'usager"
#relationship
relationship:
duplicate: Une relation de filiation existe déjà entre ces 2 personnes

View File

@ -334,7 +334,7 @@ class ReportController extends AbstractController
$cFGroupId = $request->query->get('cFGroup');
if ($cFGroupId) {
if ($request->query->has('cFGroup')) {
return $this->redirect(
$this->generateUrl(
'report_new',
@ -391,7 +391,7 @@ class ReportController extends AbstractController
{
$cFGroupId = $request->query->get('cFGroup');
if ($cFGroupId) {
if ($request->query->has('cFGroup')) {
return $this->redirect(
$this->generateUrl(
'report_export_list',

View File

@ -33,7 +33,6 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@ -494,7 +493,7 @@ final class SingleTaskController extends AbstractController
);
default:
throw new BadRequestException("format not supported: {$_format}");
throw new BadRequestHttpException("format not supported: {$_format}");
}
}