Merge remote-tracking branch 'origin/master' into features/household-validation

This commit is contained in:
2021-06-18 12:02:34 +02:00
83 changed files with 3539 additions and 647 deletions

View File

@@ -0,0 +1,18 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class HouseholdApiController extends ApiController
{
public function householdAddressApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, [ 'groups' => [ 'read' ] ]);
}
}

View File

@@ -3,6 +3,7 @@
namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Form\HouseholdType;
use Chill\MainBundle\Entity\Address;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
@@ -104,6 +105,16 @@ class HouseholdController extends AbstractController
public function addresses(Request $request, Household $household)
{
// TODO ACL
//TODO put these lines into a validator constraint on household->getAddress
$addresses = $household->getAddresses();
$cond = True;
for ($i=0; $i < count($addresses) - 1; $i++) {
if ($addresses[$i]->getValidFrom() != $addresses[$i + 1]->getValidTo()) {
$cond = False;
}
}
return $this->render('@ChillPerson/Household/addresses.html.twig',
[
'household' => $household
@@ -111,6 +122,7 @@ class HouseholdController extends AbstractController
);
}
/**
* @Route(
* "/{household_id}/address/move",
@@ -122,6 +134,7 @@ class HouseholdController extends AbstractController
public function addressMove(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/address_move.html.twig',
[
'household' => $household
@@ -177,4 +190,26 @@ class HouseholdController extends AbstractController
return $form;
}
/**
* @Route(
* "/{household_id}/address/edit",
* name="chill_person_household_address_edit",
* methods={"GET", "HEAD", "POST"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function addressEdit(Request $request, Household $household)
{
// TODO ACL
//$address = $this->findAddressById($household, $address_id); //TODO
return $this->render('@ChillPerson/Household/address_edit.html.twig',
[
'household' => $household,
//'address' => $address,
]
);
}
}

View File

@@ -45,7 +45,7 @@ class PersonAddressController extends AbstractController
* @var ValidatorInterface
*/
protected $validator;
/**
* PersonAddressController constructor.
*
@@ -55,7 +55,7 @@ class PersonAddressController extends AbstractController
{
$this->validator = $validator;
}
public function listAction($person_id)
{
$person = $this->getDoctrine()->getManager()

View File

@@ -22,7 +22,8 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Role\Role;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Chill\MainBundle\Entity\Address;
class PersonApiController extends ApiController
{
@@ -35,7 +36,7 @@ class PersonApiController extends ApiController
{
$this->authorizationHelper = $authorizationHelper;
}
protected function createEntity(string $action, Request $request): object
{
$person = parent::createEntity($action, $request);
@@ -47,4 +48,10 @@ class PersonApiController extends ApiController
return $person;
}
public function personAddressApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, [ 'groups' => [ 'read' ] ]);
}
}

View File

@@ -201,7 +201,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'workflows' => [
'accompanying_period_lifecycle' => [
'type' => 'state_machine',
'audit_trail' => [
'audit_trail' => [
'enabled' => true
],
'marking_store' => [
@@ -354,15 +354,15 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'controller' => \Chill\PersonBundle\Controller\AccompanyingCourseApiController::class,
'actions' => [
'_entity' => [
'roles' => [
'roles' => [
Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_PATCH => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_PUT => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
],
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_PUT => true,
Request::METHOD_PATCH => true,
Request::METHOD_GET => true,
Request::METHOD_PUT => true,
Request::METHOD_PATCH => true,
]
],
'participation' => [
@@ -379,61 +379,61 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
],
'resource' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'comment' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'requestor' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'scope' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'socialissue' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'socialIssueApi',
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
@@ -441,11 +441,11 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'confirm' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
]
],
@@ -510,8 +510,48 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_HEAD => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE,
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\PersonVoter::CREATE,
],
],
'address' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'personAddressApi'
],
]
],
[
'class' => \Chill\PersonBundle\Entity\Household\Household::class,
'controller' => \Chill\PersonBundle\Controller\HouseholdApiController::class,
'name' => 'household',
'base_path' => '/api/1.0/person/household',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
Request::METHOD_POST=> true,
]
],
'address' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'householdAddressApi'
],
]
],
[

View File

@@ -7,6 +7,8 @@ use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Validator\Constraints\Household\MaxHolder;
@@ -28,7 +30,7 @@ class Household
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
* @Serializer\Groups({"write"})
*/
private ?int $id = null;
@@ -40,6 +42,7 @@ class Household
* cascade={"persist", "remove", "merge", "detach"})
* @ORM\JoinTable(name="chill_person_household_to_addresses")
* @ORM\OrderBy({"validFrom" = "DESC"})
* @Serializer\Groups({"write"})
*/
private Collection $addresses;
@@ -48,7 +51,7 @@ class Household
* targetEntity=HouseholdMember::class,
* mappedBy="household"
* )
* @Serializer\Groups({"read"})
* @Serializer\Groups({"write"})
*/
private Collection $members;
@@ -87,6 +90,11 @@ class Household
{
$this->addresses[] = $address;
foreach ($this->getAddresses() as $a) {
if ($a->getValidFrom() < $address->getValidFrom() && $a->getValidTo() === NULL) {
$a->setValidTo($address->getValidFrom());
}
}
return $this;
}
@@ -102,6 +110,7 @@ class Household
* By default, the addresses are ordered by date, descending (the most
* recent first)
*
* @Assert\Callback(methods={"validate"})
* @return \Chill\MainBundle\Entity\Address[]
*/
public function getAddresses()
@@ -294,4 +303,18 @@ class Household
return $this;
}
public function validate(ExecutionContextInterface $context, $payload)
{
$addresses = $this->getAddresses();
$cond = True;
for ($i=0; $i < count($addresses) - 1; $i++) {
if ($addresses[$i]->getValidFrom() != $addresses[$i + 1]->getValidTo()) {
$cond = False;
$context->buildViolation('The address are not sequentials. The validFrom date of one address should be equal to the validTo date of the previous address.')
->atPath('addresses')
->addViolation();
}
}
dump($cond);
}
}

View File

@@ -24,28 +24,17 @@ use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class AgeAggregator implements AggregatorInterface,
ExportElementValidatedInterface
final class AgeAggregator implements AggregatorInterface, ExportElementValidatedInterface
{
/**
*
* @var
*/
protected $translator;
public function __construct($translator)
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function addRole()
{
return null;

View File

@@ -29,40 +29,23 @@ use Chill\MainBundle\Util\CountriesInfo;
use Symfony\Component\Security\Core\Role\Role;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Repository\CountryRepository;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CountryOfBirthAggregator implements AggregatorInterface,
ExportElementValidatedInterface
final class CountryOfBirthAggregator implements AggregatorInterface, ExportElementValidatedInterface
{
/**
*
* @var EntityRepository
*/
protected $countriesRepository;
private CountryRepository $countriesRepository;
/**
*
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
private TranslatableStringHelper $translatableStringHelper;
/**
*
* @var TranslatorInterface
*/
protected $translator;
private TranslatorInterface $translator;
public function __construct(EntityRepository $countriesRepository,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator)
{
public function __construct(
CountryRepository $countriesRepository,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator
) {
$this->countriesRepository = $countriesRepository;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
@@ -73,7 +56,6 @@ class CountryOfBirthAggregator implements AggregatorInterface,
return 'person';
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('group_by_level', ChoiceType::class, array(

View File

@@ -26,31 +26,20 @@ use Symfony\Component\Translation\TranslatorInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Export\Declarations;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class GenderAggregator implements AggregatorInterface
final class GenderAggregator implements AggregatorInterface
{
/**
*
* @var TranslatorInterface
*/
protected $translator;
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function applyOn()
{
return Declarations::PERSON_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{

View File

@@ -29,39 +29,23 @@ use Chill\MainBundle\Util\CountriesInfo;
use Symfony\Component\Security\Core\Role\Role;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Repository\CountryRepository;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class NationalityAggregator implements AggregatorInterface,
ExportElementValidatedInterface
final class NationalityAggregator implements AggregatorInterface, ExportElementValidatedInterface
{
/**
*
* @var EntityRepository
*/
protected $countriesRepository;
private CountryRepository $countriesRepository;
/**
*
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
private TranslatableStringHelper $translatableStringHelper;
/**
*
* @var TranslatorInterface
*/
protected $translator;
private TranslatorInterface $translator;
public function __construct(EntityRepository $countriesRepository,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator)
{
public function __construct(
CountryRepository $countriesRepository,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator
) {
$this->countriesRepository = $countriesRepository;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;

View File

@@ -0,0 +1,119 @@
<template>
<div class='household__address-move'>
<div class='household__address-move__create'>
<div>
<h2>{{ $t('create_a_new_address') }}</h2>
<add-address
@addNewAddress="addNewAddress">
</add-address>
</div>
<div>
<show-address
v-if="newAddress"
v-bind:address="newAddress">
</show-address>
</div>
</div>
<div class='household__address-move__valid'>
<h2>{{ $t('move_date') }}</h2>
<input
type="date"
name="validFrom"
:placeholder="$t('validFrom')"
v-model="validFrom"/>
<div v-if="errors.length > 0">
{{ errors }}
</div>
</div>
<div>
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a :href=backUrl class="sc-button bt-cancel">{{ $t('back_to_the_list') }}</a>
</li>
<li>
<button type="submit" class="sc-button bt-update centered" @click="addToHousehold">
{{ $t('add_an_address_to_household') }}
</button>
</li>
</ul>
</div>
</div>
</template>
<script>
import AddAddress from 'ChillMainAssets/vuejs/_components/AddAddress.vue';
import ShowAddress from 'ChillMainAssets/vuejs/_components/ShowAddress.vue';
export default {
name: 'App',
components: {
AddAddress,
ShowAddress
},
data() {
return {
edit: window.mode === 'edit',
householdId: window.householdId,
backUrl: `/fr/person/household/${householdId}/addresses`, //TODO better way to pass this
validFrom: new Date().toISOString().split('T')[0]
}
},
computed: {
newAddress() {
return this.$store.state.newAddress;
},
errors() {
return this.$store.state.errorMsg;
}
},
methods: {
addNewAddress({ address, modal }) {
console.log('@@@ CLICK button addNewAdress', address);
let createdAddress = {
'isNoAddress': address.isNoAddress,
'street': address.isNoAddress ? '' : address.street,
'streetNumber': address.isNoAddress ? '' : address.streetNumber,
'postcode': {'id': address.selected.city.id},
'floor': address.floor,
'corridor': address.corridor,
'steps': address.steps,
'flat': address.flat,
'buildingName': address.buildingName,
'distribution': address.distribution,
'extra': address.extra
};
if (address.selected.address.point !== undefined){
createdAddress = Object.assign(createdAddress, {
'point': address.selected.address.point.coordinates
});
}
if(address.writeNewPostalCode){
let newPostalCode = address.newPostalCode;
newPostalCode = Object.assign(newPostalCode, {
'country': {'id': address.selected.country.id },
});
createdAddress = Object.assign(createdAddress, {
'newPostalCode': newPostalCode
});
}
this.$store.dispatch('addAddress', createdAddress);
modal.showModal = false;
},
addToHousehold() {
this.$store.dispatch('addDateToAddressAndAddressToHousehold', {
householdId: this.householdId,
addressId: this.$store.state.newAddress.address_id,
body: { validFrom: {datetime: `${this.validFrom}T00:00:00+0100`}}
})
}
}
};
</script>

View File

@@ -0,0 +1,23 @@
/*
* Endpoint household
* method POST, post Household instance
*
* @id integer - id of household
* @body Object - dictionary with changes to post
*/
export const postAddressToHousehold = (householdId, addressId) => {
const body = {
'id': addressId
};
const url = `/api/1.0/person/household/${householdId}/address.json`
return fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json;charset=utf-8'},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};

View File

@@ -0,0 +1,47 @@
<template>
<div class="container">
<VueMultiselect
v-model="value"
@select="selectAddress"
name="field"
track-by="id"
label="value"
:custom-label="transName"
:multiple="false"
:placeholder="$t('select_address')"
:options="addresses">
</VueMultiselect>
</div>
</template>
<script>
import VueMultiselect from 'vue-multiselect';
export default {
name: 'SelectHouseholdAddress',
components: { VueMultiselect },
props: ['address'],
data() {
return {
value: null
}
},
computed: {
addresses() {
return this.address.loaded.addresses;
}
},
methods: {
transName(value) {
return `${value.text} ${value.postcode.name}`
},
selectAddress(value) {
this.address.selected.address = value;
}
}
};
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -0,0 +1,25 @@
import { createApp } from 'vue'
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
import { appMessages } from './js/i18n'
import { store } from './store'
import App from './App.vue';
const root = window.vueRootComponent;
/*
* Load all App component, for Household edition page
*/
const i18n = _createI18n(appMessages);
const app = createApp({
template: `<app></app>`,
})
.use(store)
.use(i18n)
.component('app', App)
.mount('#household-address');

View File

@@ -0,0 +1,18 @@
import { addressMessages } from 'ChillMainAssets/vuejs/Address/js/i18n'
const appMessages = {
fr: {
select_a_existing_address: 'Sélectionner une adresse existante',
create_a_new_address: 'Créer une nouvelle adresse',
add_an_address_to_household: 'Déménager le ménage',
validFrom: 'Date du déménagement',
move_date: 'Date du déménagement',
back_to_the_list: 'Retour à la liste'
}
};
Object.assign(appMessages.fr, addressMessages.fr);
export {
appMessages
};

View File

@@ -0,0 +1,90 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { postAddress, postPostalCode, patchAddress } from 'ChillMainAssets/vuejs/_api/AddAddress';
import { postAddressToHousehold } from '../api';
const debug = process.env.NODE_ENV !== 'production';
const store = createStore({
strict: debug,
state: {
newAddress: {},
household: {},
validFrom: {},
errorMsg: []
},
getters: {
},
mutations: {
catchError(state, error) {
state.errorMsg.push(error);
},
addAddress(state, address) {
console.log('@M addAddress address', address);
state.newAddress = address;
},
addAddressToHousehold(state, household) {
console.log('@M addAddressToHousehold household', household);
state.household = household;
},
addDateToAddress(state, validFrom) {
console.log('@M addDateToAddress address.validFrom', validFrom);
state.validFrom = validFrom;
}
},
actions: {
addAddress({ commit }, payload) {
console.log('@A addAddress payload', payload);
if('newPostalCode' in payload){
postPostalCode(payload.newPostalCode)
.then(postalCode => {
let body = payload;
body.postcode = {'id': postalCode.id },
postAddress(body)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
})
} else {
postAddress(payload)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
}
},
addDateToAddressAndAddressToHousehold({ commit }, payload) {
console.log('@A addDateToAddressAndAddressToHousehold payload', payload);
patchAddress(payload.addressId, payload.body)
.then(address => new Promise((resolve, reject) => {
commit('addDateToAddress', address.validFrom);
resolve();
}).then(
postAddressToHousehold(payload.householdId, payload.addressId)
.then(household => new Promise((resolve, reject) => {
commit('addAddressToHousehold', household);
resolve();
}))
.catch((error) => {
commit('catchError', error);
})
))
.catch((error) => {
commit('catchError', error);
});
},
}
});
export { store };

View File

@@ -18,31 +18,27 @@
{% set activeRouteKey = '' %}
{% block title 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) %}
{% block title 'Modify address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) %}
{% block personcontent %}
<h1>{{ 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
{% block content %}
<h1>{{ block('title') }}</h1>
<div id="address"></div>
{% endblock %}
{{ form_start(form) }}
{% block stylesheets %}
<link href="{{ asset('build/address.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}
{{ form_row(form.isNoAddress) }}
{{ form_row(form.street) }}
{{ form_row(form.streetNumber) }}
{{ form_row(form.postCode) }}
{{ form_row(form.validFrom) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_person_address_list', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
{{ form_row(form.submit, { 'attr' : { 'class': 'sc-button bt-save' }, 'label': 'Save' } ) }}
</li>
</ul>
{{ form_end(form) }}
{% block js %}
<script type="text/javascript">
window.personId = {{ person.id|e('js') }};
window.addressId = {{ address.id|e('js') }};
window.mode = 'edit';
window.vueRootComponent = 'app';
</script>
{{ encore_entry_script_tags('address') }}
{% endblock %}
{% endblock personcontent %}

View File

@@ -26,64 +26,76 @@
<h1>{{ 'Addresses\'history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
<table class="records_list">
<thead>
<tr>
<th>{{ 'Valid from'|trans }}</th>
<th>{{ 'Address'|trans }}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% if person.addresses|length == 0 %}
<tr>
<td colspan="3">
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
<a href="{{ path('chill_person_address_new', { 'person_id' : person.id } ) }}">
<div class="person__address">
<div class="row">
<div class="person__address--date"></div>
<div class="person__address--content">
<div class="cell">
<div>
{% if person.addresses|length == 0 %}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{% endif %}
<a class="sc-button bt-create"
href="{{ path('chill_person_address_new', { 'person_id' : person.id } ) }}">
{{ 'Add an address'|trans }}
</a>
</td>
</tr>
{% else %}
{% for address in person.addresses %}
<tr>
<td><strong>{{ 'Since %date%'|trans( { '%date%' : address.validFrom|format_date('long') } ) }}</strong></td>
</a>
</div>
</div>
</div>
</div>
<td>
{{ address_macros._render(address, { 'with_valid_from' : false, 'has_no_address': true } ) }}
</td>
{% for address in person.addresses %}
<div class="row">
<div class="person__address--date">
<div class="cell">
<div class="pill">
{% if address.validFrom is not empty %}
{{ address.validFrom|format_date('long') }}
{% endif %}
</div>
</div>
</div>
<div class="person__address--content">
<div class="cell">
<i class="dot"></i>
<div>
{% if address.isNoAddress == true %}
<div class="chill_address_is_noaddress">{{ 'address.consider homeless'|trans }}</div>
{% else %}
<div>
{% if address.street is not empty %}
<div class="street">
<i class="fa fa-fw fa-map-marker"></i>
<span class="streetNumber">{{ address.street }}</span>
{% if address.streetNumber is not empty %}
<span class="streetNumber">, {{ address.streetNumber }}</span>
{% endif %}
</div>
{% endif %}
{% if address.postCode is not empty %}
<div class="postCode">
<span>{{ address.postCode.code }}</span> <span>{{ address.postCode.name }}</span>
<span class="country">({{ address.postCode.country.name|localize_translatable_string }})</span>
</div>
{% endif %}
</div>
{% endif %}
<a href="{{ path('chill_person_address_edit', { 'person_id': person.id, 'address_id' : address.id } ) }}" class="sc-button bt-edit"></a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_person_address_edit', { 'person_id': person.id, 'address_id' : address.id } ) }}" class="sc-button bt-edit"></a>
</li>
</ul>
</td>
</tr>
<ul class="record_actions">
<li class="cancel">
<a href="{{ path('chill_person_view', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the person details'|trans }}
</a>
</li>
</ul>
{% endfor %}
{% endif %}
</tbody>
</table>
<ul class="record_actions">
<li class="cancel">
<a href="{{ path('chill_person_view', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the person details'|trans }}
</a>
</li>
<li>
<a href="{{ path('chill_person_address_new', { 'person_id' : person.id } ) }}" class="sc-button bt-create">
{{ 'Add an address'|trans }}
</a>
</li>
</ul>
{% endblock personcontent %}

View File

@@ -22,36 +22,6 @@
{% block personcontent %}
<h1>{{ 'New address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
{{ form_start(form) }}
{{ form_row(form.isNoAddress) }}
{{ form_row(form.street) }}
{{ form_errors(form.street) }}
{{ form_row(form.streetNumber) }}
{{ form_errors(form.streetNumber) }}
{{ form_row(form.postCode) }}
{{ form_errors(form.postCode) }}
{{ form_row(form.validFrom) }}
{{ form_errors(form.validFrom) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_person_address_list', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
{{ form_row(form.submit, { 'attr' : { 'class': 'sc-button bt-create' }, 'label': 'Create' } ) }}
</li>
</ul>
{{ form_end(form) }}
NEW FORM
{% block content %}
<h1>{{ block('title') }}</h1>
<div id="address"></div>
@@ -62,6 +32,11 @@
{% endblock %}
{% block js %}
<script type="text/javascript">
window.personId = {{ person.id|e('js') }};
window.mode = 'new';
window.vueRootComponent = 'app';
</script>
{{ encore_entry_script_tags('address') }}
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Edit household address'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<div>
<div id="household-address"></div>
</div>
{% block stylesheets %}
<link href="{{ asset('build/address.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}
{% block js %}
<script type="text/javascript">
window.householdId = {{ household.id|e('js') }};
window.addressId = {{ address.id|e('js') }};
window.mode = 'edit';
window.vueRootComponent = 'app';
</script>
{{ encore_entry_script_tags('household_address') }}
{% endblock %}
{% endblock %}

View File

@@ -3,9 +3,22 @@
{% block title 'Move household'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<h1>{{ block('title') }}</h1>
<p>Household with id {{ household.id }}</p>
<div>
<div id="household-address"></div>
</div>
{% block stylesheets %}
<link href="{{ asset('build/address.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}
{% block js %}
<script type="text/javascript">
window.householdId = {{ household.id|e('js') }};
window.vueRootComponent = 'app';
</script>
{{ encore_entry_script_tags('household_address') }}
{% endblock %}
{% endblock %}

View File

@@ -4,12 +4,67 @@
{% block content %}
<h1>{{ block('title') }}</h1>
<div class="household">
<p>Household with id {{ household.id }}</p>
<div class="household__address">
<a class="sc-button bt-update"
href="{{ chill_path_add_return_path('chill_person_household_address_move', { 'household_id': household.id }) }}">
{{ 'Move household'|trans }}
</a>
<div class="row">
<div class="household__address--date"></div>
<div class="household__address--content">
<div class="cell">
<a class="sc-button bt-create"
href="{{ chill_path_add_return_path('chill_person_household_address_move', { 'household_id': household.id }) }}">
{{ 'Move household'|trans }}
</a>
</div>
</div>
</div>
{% for address in household.addresses %}
<div class="row">
<div class="household__address--date">
<div class="cell">
<div class="pill">
{% if address.validFrom is not empty %}
{{ address.validFrom|format_date('long') }}
{% endif %}
</div>
</div>
</div>
<div class="household__address--content">
<div class="cell">
<i class="dot"></i>
<div>
{% if address.isNoAddress == true %}
<div class="chill_address_is_noaddress">{{ 'address.consider homeless'|trans }}</div>
{% else %}
<div>
{% if address.street is not empty %}
<div class="street">
<i class="fa fa-fw fa-map-marker"></i>
<span class="streetNumber">{{ address.street }}</span>
{% if address.streetNumber is not empty %}
<span class="streetNumber">, {{ address.streetNumber }}</span>
{% endif %}
</div>
{% endif %}
{% if address.postCode is not empty %}
<div class="postCode">
<span>{{ address.postCode.code }}</span> <span>{{ address.postCode.name }}</span>
<span class="country">({{ address.postCode.country.name|localize_translatable_string }})</span>
</div>
{% endif %}
</div>
{% endif %}
<a href="{{ path('chill_person_household_address_edit', { 'household_id': household.id, 'address_id' : address.id } ) }}" class="sc-button bt-edit"></a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -2,8 +2,8 @@
/*
* Chill is a software for social workers
*
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
*
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
* <http://www.champs-libres.coop>, <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
@@ -54,7 +54,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
{
static::bootKernel();
}
/**
* Setup before each test method (see phpunit doc)
*/
@@ -76,11 +76,11 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
$data = \json_decode($response->getContent());
$this->assertEquals($data->id, $period->getId(),
"test that the response's data contains the id of the period"
);
);
$this->assertGreaterThan(0, $data->participations);
}
@@ -106,7 +106,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
[],
\json_encode([ 'type' => 'social_issue', 'id' => $si->getId() ])
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$data = \json_decode($this->client->getResponse()->getContent(), true);
@@ -204,7 +204,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
// post a third party
// post a third party
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/resource.json', $period->getId()),
@@ -269,7 +269,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$this->assertInstanceOf(Person::class, $period->getRequestor());
$this->assertEquals($personId, $period->getRequestor()->getId());
// post a third party
// post a third party
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
@@ -332,7 +332,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$em = self::$container->get(EntityManagerInterface::class);
$this->client->request(
Request::METHOD_PATCH,
Request::METHOD_PATCH,
sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()),
[], // parameters
[], // files
@@ -368,7 +368,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
->setParameter('center', $center)
->setMaxResults($maxResults)
->getScalarResult();
// create a random order
shuffle($personIds);
@@ -376,7 +376,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
ThirdParty::class." t ")
->setMaxResults($maxResults)
->getScalarResult();
// create a random order
shuffle($thirdPartyIds);
@@ -421,7 +421,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
// check by deownloading the accompanying cours
$this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()));
$response = $this->client->getResponse();
$data = \json_decode($response->getContent());
@@ -491,9 +491,9 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
public function dataGenerateRandomAccompanyingCourseWithSocialIssue()
{
// note about max result for person query, and maxGenerated:
//
//
// in the final loop, an id is popped out of the personIds array twice:
//
//
// * one for getting the person, which will in turn provide his accompanying period;
// * one for getting the personId to populate to the data manager
//
@@ -512,7 +512,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
->setParameter('center', $center)
->setMaxResults($maxResults)
->getScalarResult();
// create a random order
shuffle($personIds);
@@ -538,9 +538,9 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
public function dataGenerateRandomAccompanyingCourse()
{
// note about max result for person query, and maxGenerated:
//
//
// in the final loop, an id is popped out of the personIds array twice:
//
//
// * one for getting the person, which will in turn provide his accompanying period;
// * one for getting the personId to populate to the data manager
//
@@ -553,13 +553,26 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$center = $em->getRepository(Center::class)
->findOneBy(array('name' => 'Center A'));
$personIds = $em->createQuery("SELECT p.id FROM ".
Person::class." p ".
" WHERE p.center = :center")
$qb = $em->createQueryBuilder();
$personIds = $qb
->select('p.id')
->from(Person::class, 'p')
->where(
$qb->expr()->eq(
'p.center',
':center'
)
)
->andWhere(
$qb->expr()->gt(
'SIZE(p.accompanyingPeriodParticipations)',
0)
)
->setParameter('center', $center)
->setMaxResults($maxResults)
->getQuery()
->getScalarResult();
// create a random order
shuffle($personIds);
@@ -597,7 +610,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
->setParameter('center', $center)
->setMaxResults(100)
->getScalarResult();
// create a random order
shuffle($personIds);

View File

@@ -179,7 +179,7 @@ components:
readOnly: true
children_ids:
type: array
items:
items:
type: integer
readOnly: true
title:
@@ -210,7 +210,7 @@ components:
type: string
enum:
- 'household_position'
paths:
/1.0/person/person/{id}.json:
@@ -259,10 +259,48 @@ paths:
description: "Unauthorized"
422:
description: "Invalid data: the data is a valid json, could be deserialized, but does not pass validation"
/1.0/person/person/{id}/address.json:
post:
tags:
- person
summary: post an address to a person
parameters:
- name: id
in: path
required: true
description: The person id
schema:
type: integer
format: integer
minimum: 1
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: integer
description: The address id to attach to the person
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
/1.0/person/social-work/social-issue.json:
get:
tags:
tags:
- social-issue
summary: Return a list of social work
responses:
@@ -270,7 +308,7 @@ paths:
description: "ok"
/1.0/person/social-work/social-issue/{id}.json:
get:
tags:
tags:
- social-issue
summary: Return a social issue by id
parameters:
@@ -424,7 +462,7 @@ paths:
description: "OK"
422:
description: "object with validation errors"
/1.0/person/accompanying-course/{id}/participation.json:
post:
tags:
@@ -784,6 +822,41 @@ paths:
400:
description: "transition cannot be applyed"
/1.0/person/household.json:
get:
tags:
- household
summary: Return a list of all household
responses:
200:
description: "ok"
/1.0/person/household/{id}.json:
get:
tags:
- household
summary: Return a household by id
parameters:
- name: id
in: path
required: true
description: The household id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/Household'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/person/household/members/move.json:
post:
tags:
@@ -868,4 +941,39 @@ paths:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
/1.0/person/household/{id}/address.json:
post:
tags:
- household
summary: post an address to a household
parameters:
- name: id
in: path
required: true
description: The household id
schema:
type: integer
format: integer
minimum: 1
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: integer
description: The address id to attach to the household
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"

View File

@@ -7,9 +7,10 @@ module.exports = function(encore, entries)
encore.addAliases({
ChillPersonAssets: __dirname + '/Resources/public'
});
encore.addEntry('accompanying_course', __dirname + '/Resources/public/vuejs/AccompanyingCourse/index.js');
encore.addEntry('household_members_editor', __dirname + '/Resources/public/vuejs/HouseholdMembersEditor/index.js');
encore.addEntry('vue_accourse', __dirname + '/Resources/public/vuejs/AccompanyingCourse/index.js');
encore.addEntry('household_edit_metadata', __dirname + '/Resources/public/modules/household_edit_metadata/index.js');
encore.addEntry('household_address', __dirname + '/Resources/public/vuejs/HouseholdAddress/index.js');
};

View File

@@ -73,6 +73,11 @@ services:
resource: '../Controller/'
tags: ['controller.service_arguments']
Chill\PersonBundle\Export\:
autowire: true
autoconfigure: true
resource: '../Export/'
Chill\PersonBundle\Templating\Entity\:
autowire: true
autoconfigure: true

View File

@@ -36,43 +36,39 @@ services:
chill.person.export.filter_birthdate:
class: Chill\PersonBundle\Export\Filter\BirthdateFilter
tags:
- { name: chill.export_filter, alias: person_birthdate_filter }
- { name: chill.export_filter, alias: person_birthdate_filter }
chill.person.export.filter_nationality:
class: Chill\PersonBundle\Export\Filter\NationalityFilter
arguments:
- "@chill.main.helper.translatable_string"
autowire: true
autoconfigure: true
tags:
- { name: chill.export_filter, alias: person_nationality_filter }
chill.person.export.aggregator_nationality:
class: Chill\PersonBundle\Export\Aggregator\NationalityAggregator
arguments:
- "@chill.main.countries_repository"
- "@chill.main.helper.translatable_string"
- "@translator"
autowire: true
autoconfigure: true
tags:
- { name: chill.export_aggregator, alias: person_nationality_aggregator }
chill.person.export.aggregator_country_of_birth:
class: Chill\PersonBundle\Export\Aggregator\CountryOfBirthAggregator
arguments:
- "@chill.main.countries_repository"
- "@chill.main.helper.translatable_string"
- "@translator"
autowire: true
autoconfigure: true
tags:
- { name: chill.export_aggregator, alias: person_country_of_birth_aggregator }
chill.person.export.aggregator_gender:
class: Chill\PersonBundle\Export\Aggregator\GenderAggregator
arguments:
- "@translator"
autowire: true
autoconfigure: true
tags:
- { name: chill.export_aggregator, alias: person_gender_aggregator }
chill.person.export.aggregator_age:
class: Chill\PersonBundle\Export\Aggregator\AgeAggregator
arguments:
- "@translator"
autowire: true
autoconfigure: true
tags:
- { name: chill.export_aggregator, alias: person_age_aggregator }

View File

@@ -187,6 +187,7 @@ Pick a person: Choisir une personne
No address given: Pas d'adresse renseignée
The address has been successfully updated: L'adresse a été mise à jour avec succès
Update address for %name%: Mettre à jour une adresse pour %name%
Modify address for %name%: Modifier une adresse pour %name%
Addresses'history for %name%: Historique des adresses de %name%
Addresses'history: Historique des adresses
New address for %name% : Nouvelle adresse pour %name%
@@ -319,3 +320,11 @@ Show Accompanying Course: Voir le parcours
Edit Accompanying Course: Modifier le parcours
Create Accompanying Course: Créer un nouveau parcours
Drop Accompanying Course: Supprimer le parcours
# Household
Household: Ménage
Summary: Résumé
Members: Membres
Addresses: Adresses
Move household: Nouveau déménagement
Addresses history for household: Historique des adresses