Compare commits

...

83 Commits

Author SHA1 Message Date
5f026068e1 fix capitalization 2021-10-14 14:40:36 +02:00
0497625e90 fix capitalization 2021-10-14 14:38:36 +02:00
8c3f8d8806 try to handle the center input by using default value 2021-10-14 14:29:27 +02:00
aa49777d7a fix capitalization in personcontroller create test 2021-10-14 14:26:09 +02:00
f3ccc96e41 fix capitalization of MANçO 2021-10-14 14:24:45 +02:00
6adcdaa69a remove print in load fixtures 2021-10-14 14:20:18 +02:00
a96e5e25ce capitalize searched names 2021-10-14 14:15:33 +02:00
a75f4015c7 Merge branch 'features/thirdparty-improve-acl-collection' into 'master'
3party, suite

See merge request Chill-Projet/chill-bundles!173
2021-10-14 12:06:51 +00:00
eaeb6c18b0 try to fix loading of reports fixtures 2021-10-14 13:57:42 +02:00
9a02665053 some tweaks into PickAddressType 2021-10-14 13:54:19 +02:00
6fccc70e41 add some doc [ci-skip] 2021-10-13 23:02:53 +02:00
13b96637bb Refactor address vue app and create a PickAddressType 2021-10-13 22:58:54 +02:00
01ff88074b add parent in thirdpartyrenderbox.vue 2021-10-12 17:50:32 +02:00
b84d0100e0 create style for badge 3party and introduce in ThirdPartyRenderBox.vue 2021-10-12 17:32:17 +02:00
88d073b9a9 handle types and categories in a single select input 2021-10-12 15:52:06 +02:00
9eec15873e fixtures for civility: add abbreviations 2021-10-12 09:44:51 +02:00
e9192c5011 adaptations for third party search 2021-10-12 09:37:35 +02:00
f4369553e1 fix layout for address 2021-10-12 09:37:35 +02:00
bc608832c2 fill changelog
[ci-skip]
2021-10-12 09:37:35 +02:00
313c17826e translation of company & contact and show parent in list 2021-10-12 09:37:35 +02:00
acbb33918f improve translation 2021-10-12 09:37:35 +02:00
cb29e1e8be Update CONVENTIONS.md [ci-skip] 2021-10-11 18:44:25 +00:00
799ed0de5c Add new file 2021-10-11 18:43:39 +00:00
76f84934d4 update changelog with today release 2021-10-11 17:36:20 +02:00
992b320820 replace continue instruction by equivalent break in switch 2021-10-11 15:01:37 +02:00
47897b5fe3 update changelog [ci-skip] 2021-10-11 14:54:08 +02:00
b5394a3de2 use entity_render_string in titles 2021-10-11 14:53:04 +02:00
2db9a98bfe fix bootstrapping listener for person capitalization name 2021-10-11 14:53:04 +02:00
0c5e5ac672 Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles 2021-10-11 14:42:57 +02:00
ffb99e26fb changelog correction 2021-10-11 14:41:18 +02:00
a27b228a6f Merge branch 'features/thirdparty-improve-acl-collection' 2021-10-11 14:31:47 +02:00
abedf83dbd fix flushing after person creation 2021-10-11 14:31:03 +02:00
80a05f76ba update changelog [ci-skip] 2021-10-11 14:17:35 +02:00
0dbff5a6a7 rename tests for PickCenterType and desactivate them 2021-10-11 14:08:36 +02:00
d7ae279101 Create thirdparty on the fly with institution / contact 2021-10-11 14:08:36 +02:00
08764aa0b4 improve layout of third parties in AddPerson 2021-10-11 14:08:36 +02:00
c54edd8407 fix civility repository namespace 2021-10-11 14:08:36 +02:00
e465170c7b work on render box and list 2021-10-11 14:08:36 +02:00
72e9346b6b fix loading third party 2021-10-11 14:08:36 +02:00
0633fd812f handle kind as contacts 2021-10-11 14:08:36 +02:00
4d71a1c630 add possibility to generate filter/order elements, with only search box
for now
2021-10-11 14:08:36 +02:00
e286acf9fe fix namespace 2021-10-11 14:08:36 +02:00
7812442c9a page vue des tiers 2021-10-11 14:08:36 +02:00
9a708ca618 fix canonicalization and show only active children in form 2021-10-11 14:08:36 +02:00
58ddbfb67b remove occurences for ThirdPartyCivility 2021-10-11 14:08:36 +02:00
2820ad83de add trigger for canonicalization and anonymouse flag on 3party 2021-10-11 14:08:36 +02:00
ca3ced0308 various improvements on 3party 2021-10-11 14:08:36 +02:00
7ea2c36c5e crudification for thirdparty 2021-10-11 14:08:36 +02:00
a8edef13a3 refactor centerType 2021-10-11 14:08:36 +02:00
05b9476a71 crudification + corrections on thirdparty 2021-10-11 14:08:36 +02:00
cc7e38194f Merge branch 'submit_eventlistener' into 'master'
Submit eventlistener

See merge request Chill-Projet/chill-bundles!171
2021-10-11 07:41:03 +00:00
bfe5e0f515 Merge branch 'names_to_caps' into 'master'
Names to caps

See merge request Chill-Projet/chill-bundles!167
2021-10-08 16:43:29 +00:00
7ed61398ea Merge branch '207_activity_bugfix' into 'master'
option added to distinguish between edit and new action so that modification...

See merge request Chill-Projet/chill-bundles!168
2021-10-08 16:43:10 +00:00
1eba6a35aa Merge branch 'master' into resume-page 2021-10-08 17:56:27 +02:00
158fbfd241 updating changelog 2021-10-08 17:43:15 +02:00
02268792ba Activity: sharing badge-title include in list and in recent-list 2021-10-08 17:32:26 +02:00
afe076041f rename recent_list to list_recent 2021-10-08 17:23:52 +02:00
800ab896bc Activity, improve badge-title. Fix too long badges line break in a narrow inline wrapper. 2021-10-08 17:15:58 +02:00
8f889248fb fix some limits with float-button asset 2021-10-08 13:01:44 +02:00
e12e5798ea refactor activity list css styles (wip) 2021-10-08 13:01:34 +02:00
52c1a01980 Refactoring sass chill colors variables and badge mixins 2021-10-08 13:01:10 +02:00
84189faae6 Translation for Last Items 2021-10-08 13:01:10 +02:00
3a07fa4faf resume short list style abstraction for accourse-work and activities (wip) 2021-10-08 13:01:04 +02:00
c87eda32cb Merge branch 'improve_address' into 'master'
Improve address

See merge request Chill-Projet/chill-bundles!169
2021-10-08 10:40:46 +00:00
nobohan
2d4b6d1dd1 minor correction following MR review + Changelog 2021-10-08 12:14:45 +02:00
8fca704f74 adjustment so that altnames are fully capitalized 2021-10-07 17:05:09 +02:00
d524897838 entrypoint added to webpack and js included in create template for person 2021-10-07 16:57:22 +02:00
fb05d892a2 reorganization of files 2021-10-07 16:16:01 +02:00
48e59bac79 changelog updated 2021-10-07 15:50:35 +02:00
nobohan
665b0f8533 postal code: adapt fixtures 2021-10-07 13:48:05 +02:00
nobohan
748fe28f8d postal code: rename fields to avoid confusion in load fixtures 2021-10-07 13:42:26 +02:00
nobohan
fd73fac1a8 adapt migration 2021-10-07 12:32:26 +02:00
aa852ec924 minor changes review: comments removed, logicexception, more test cases passed 2021-10-07 11:13:38 +02:00
9d6958a835 logic moved to controller instead of form 2021-10-07 10:56:10 +02:00
nobohan
3770318e6a enlarge the loadpostalcode command to more fields 2021-10-06 21:48:56 +02:00
nobohan
24320cb702 address: alter postalcode entity 2021-10-06 17:28:07 +02:00
7dd42c3fa2 accented letters in majascule 2021-10-06 15:52:11 +02:00
b510d6a7f5 option added to distinguish between edit and new action so that modification of activity with uactive type is possible 2021-10-06 14:07:26 +02:00
72d67624a2 minor fixes 2021-10-06 12:49:15 +02:00
Julie Lenaerts
0c4ea81550 eventListener created and tested for firstname, lastname and altnames 2021-10-06 12:16:45 +02:00
Julie Lenaerts
7d8f380050 entity listener added to person to capitalize names 2021-10-05 13:57:04 +02:00
af4cf55046 start of entity listener for person name capitalization 2021-10-04 19:33:12 +02:00
369a29af48 accompanying course work, specific render for list page or resume page (wip) 2021-10-04 14:22:27 +02:00
136 changed files with 4772 additions and 2105 deletions

View File

@@ -10,14 +10,34 @@ and this project adheres to
## Unreleased
* [3party]: french translation of contact and company
* [3party]: show parent in list
* [3party]: change color for badge "child"
## Test releases
### Test release 2021-10-11
* add 3 new fields to PostalCode and adapt postal code command and fixtures
* [Aside activity] Fixes for aside activity
* categories with child
* fast creation buttons
* add ordering for types
* [AccompanyingCourse Resume page] badge-title for AccompanyingCourseWork and for Activities;
* Improve badges behaviour with small screens;
## Test releases
* [ThirdParty]:
* third party list
* create a kind contact/institution when create a new thirdparty, and set contact embedded as kind=child;
* filter thirdparties in list
* [FilterOrder]: add development kit for generating filter and ordering in list
* [Capitalization of names] person names are capitalized on creation, on prePersist event
### test release 2021-10-04
@@ -30,14 +50,18 @@ and this project adheres to
* a badge "requestor" is shown into search results;
* periods where the person is only requestor (without participating) are also shown;
Issues:
* https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/13
* https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/199
* [Person form] "accept sms" not required:
https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/37
https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/221
* On-The-Fly modale works for showing, editing and creating person and thirdparty ;
* AccompanyingCourse Resume page: list associated persons by household, see household when hover, and show on-the-fly modale when clicking on person ;
* [On-The-Fly] modale works for showing, editing and creating person or thirdparty ;
* [AccompanyingCourse Resume page] associated persons list, can see household when hover, and with show on-the-fly modale when clicking person ;
## Test release yyyy-mm-dd

383
CONVENTIONS.md Normal file
View File

@@ -0,0 +1,383 @@
# Conventions Chill
en cours de rédaction
## Assets: nommage des entrypoints
Trois types d'entrypoint:
* application vue (souvent spécifique à une page) -> préfixé par `vue_`;
* code js/css qui est réutilisé à plusieurs endroits:
* ckeditor
* async_upload (utilisé pour un formulaire)
* bootstrap
* chill.js
* ...
=> on préfixe `mod_`
* code css ou js pour une seule page
* ré-utilise parfois des "foncitionnalités": ShowHide, ...
=> on préfixe `page_`
Arborescence:
```
# Sous Resources/public
- chill/ => theme (chill)
- chillmain.scss -> push dans l'entrypoint chill
- lib/ => ne vont jamais dans un entrypoint, mais sont ré-utilisés par d'autres
- ShowHide
- Collection
- Select2
- module/ => termine dans des entrypoints ré-utilisables (mod_)
- bootstrap
- custom.scss
- custom/
- variables.scss
- ..
- forkawesome
- AsyncUpload
- vue/ => uniquement application vue (vue_)
- _components
- app
- page/ => uniquement pour une seule page (page_)
- login
- person
- personvendee
- household_edit_metadata
- index.js
```
## Organisation des feuilles de styles
Comment s'échaffaudent les styles dans Chill ?
1. l'entrypoint **mod_bootstrap** (module bootstrap) est le premier niveau. Toutes les parties(modules) de bootstrap sont convoquées dans le fichier ```bootstrap.js``` situé dans ```ChillMainBundle/Resources/public/module/bootstrap```.
* Au début, ce fichier importe le fichier ```variables.scss``` qui détermine la plupart des réglages bootstrap tels qu'on les a personnalisés. Ce fichier surcharge l'original, et de nombreuses variables y sont adaptées pour Chill.
* On veillera à ce qu'on puisse toujours comparer ce fichier à l'original de bootstrap. En cas de mise à jour de bootstrap, il faudra générer un diff, et adapter ce diff sur le fichier variable de la nouvelle version.
* A la fin on importe le fichier ```custom.scss```, qui comprends des adaptations de bootstrap pour le préparer à notre thème Chill.
* ce ```custom.scss``` peut être splitté en plus petits fichiers avec des ```@import 'custom/...'```
* L'idée est que cette première couche bootstrap règle un partie importante des styles de l'application, en particulier ce qui touche aux position du layout, aux points de bascules responsive, aux marges et écarts appliqués par défauts aux éléments qu'on manipule.
2. l'entrypoint **chill** est le second niveau. Il contient le thème Chill qui est reconnaissable à l'application.
* Chaque bundle a un dossier ```Resources/public/chill``` dans lequel on peut trouver une feuille sass principale, qui est éventuellement splittée avec des ```@imports```. Toutes ces feuilles sont compilées dans un unique entrypoint Chill, c'est le thème de l'application. Celui-ci surcharge bootstrap.
* La feuille chillmain.scss devrait contenir les cascades de styles les plus générales, celles qui sont appliquées à de nombreux endroits de l'application.
* La feuille chillperson.scss va aussi retrouver des styles propres aux différents contextes des personnes: person, household et accompanyingcourse.
* Certains bundles plus secondaires ne contiennent que des styles spécifiques à leur fonctionnement.
3. les entrypoints **vue_** sont utilisés pour des composants vue. Les fichiers vue peuvent contenir un bloc de styles scss. Ce sont des styles qui ne concernent que le composant et son héritage, le tag ```scoped``` précise justement sa portée (voir la doc).
4. les entrypoints **page_** sont utilisés pour ajouter des assets spécifiques à certaines pages, le plus souvent des scripts et des styles.
## Taguer du code html et construire la cascade de styles
L'exemple suivant montre comment taguer sans excès un élément de code. On remarque que:
* il n'est pas nécessaire de taguer toutes les classes intérieures,
* il ne faut pas répéter la classe parent dans toutes les classes enfants. La cascade sass va permettre de saisir le html avec souplesse sans alourdir la structure des balises.
* souvent la première classe sera déclinée par plusieurs classes qui commencent de la même manière: ```bloc-dark``` ajoute juste la version sombre de ```bloc```, on ne met pas ```bloc dark```, car on ne souhaite pas que la classe ```dark``` de ```bloc``` interagisse avec la même classe ```dark``` de ```table```. On aura donc un élément ```bloc bloc-dark``` et un élément ```table table-dark```.
```html
<div class="bloc bloc-dark mon-bloc">
<h3>mon titre</h3>
<ul class="record_actions">
<li>
<a class="btn btn-edit"></a>
</li>
<li></li>
<li></li>
</ul>
</div>
```
Finalement, il importe ici de définir ce qu'est un bloc, ce qu'est une zone d'actions et ce qu'est un bouton. Ces 3 éléments existent de manière autonome, ce sont les seuls qu'on tagge.
Par exemple pour mettre un style au titre on précise juste h3 dans la cascade bloc.
```sass
div.bloc {
// un bloc générique, utilisé à plusieurs endroits
&.bloc-dark {
// la version sombre du bloc
}
h3 {}
ul {
// une liste standard dans bloc
li {
// des items de liste standard dans bloc
}
}
}
div.mon-bloc {
// des exceptions spécifiques à mon-bloc,
// qui sont des adaptations de bloc
}
ul.record_actions {
// va uniformiser tous les record_actions de l'application
li {
//...
}
}
.btn {
// les boutons de bootstrap
.btn-edit {
// chill étends les boutons bootstrap pour ses propres besoins
}
}
</style>
```
## Render box
## URL
### Nommage des routes
:::warning
Ces règles n'ont pas toujours été utilisées par le passé. Elles sont souhaitées pour le futur.
:::
Les routes sont nommées de cette manière:
`chill_bundle_entite_action`
1. d'abord chill_ (pour tous les modules chill)
2. ensuite une string qui est identique, par bundle
3. si le point est un point d'api (json), alors ajouter la string `api`
4. ensuite une string qui indique sur quelle entité porte la route, voire également les sous-entités
5. ensuite une action (`list`, `view`, `edit`, `new`, ...)
Le fait d'indiquer `api` en 3 permet de distinguer les routes d'api qui sont générées par la configuration (qui sont toutes préfixées par `chill_api`, de celles générées manuellement. (Exemple: `chill_api_household__index`, et `chill_person_api_household_members_move`)
Si les points 4 et 5 sont inexistants, alors ils sont remplacés par d'autres éléments de manière à garantir l'unicité de la route, et sa bonne compréhension.
### URL
Les URL respectent également une convention:
#### Pour les pages html
:::warning
Ces règles n'ont pas toujours été utilisées par le passé. Elles sont souhaitées pour le futur.
:::
Syntaxe:
```
/{_locale}/bundle/entity/{id}/action
/{_locale}/bundle/entity/sub-entity/{id}/action
```
Les éléments suivants devraient se trouver dans la liste:
1. la locale;
2. un identifiant du bundle
3. l'entité auquel il se rapporte
4. les éventuelles sous-entités auxquelles l'url se rapport
5. l'action
Ces éléments peuvent être entrecoupés de l'identifiant d'une entité. Dans ce cas, cet identifiant se place juste après l'entité auquel il se rapporte.
Exemple:
```
# liste des échanges pour une personne
/fr/activity/person/25/activity/list
# nouvelle activité
/fr/activity/activity/new?person_id=25
```
#### Pour les API
:::info
Les routes générées automatiquement sont préfixées par chill_api
:::
Syntaxe:
```
/api/1.0/bundle/entity/{id}/action
/api/1.0/bundle/entity/sub-entity/{id}/action
```
Les éléments suivants devraient se trouver dans la liste:
1. la string `/api/` et puis la version (1.0)
2. un identifiant du bundle
3. l'entité auquel il se rapporte
4. les éventuelles sous-entités auxquelles l'url se rapport
5. l'action
Ces éléments peuvent être entrecoupés de l'identifiant d'une entité. Dans ce cas, cet identifiant se place juste après l'entité auquel il se rapporte.
## Règles UI chill
### Titre des pages
#### Chaque page contient un titre
Chaque page contient un titre dans la balise head. Ce titre est normalement identique à celui de l'entête de la page.
Astuce: il est possible d'utiliser la fonction `block` de twig pour cela:
```htmlmixed=
{% block title "Titre de la page" %}
{% block content %}
<h1>
{{ block('title')}}
</h1>
{% endblock %}
```
### Utilisation des `entity_render`
#### En twig
Les templates twig doivent toujours utiliser la fonction chill_entity_render_box pour effectuer le rendu des éléments suivants:
* User
* Person
* SocialIssue
* SocialAction
* Address
* ThirdParty
* ...
Exemple:
```
address|chill_entity_render_box
```
Justification:
* des éléments sont parfois personnalisés par installation (par exemple, le nom de chaque utilisateur sera suivi par le nom du service)
* pour rationaliser et rendre semblable les affichages
* pour simplifier le code twig
A prevoir:
* toujours trois positions:
* inline
* block
* item (dans un tableau, une ligne)
> block et item sont en fait la même option passée au render_box: render: bloc. Il y a aussi raw pour le inline, et label pour une titraille configurable avec des options.
> quand on passe loption render: bloc, on peut placer le render_box dans une boucle for plus large qui fonctionne avec la classe flex-table ou la classe flex-bloc, ce qui donnera un affichage en rangée (table) ou en blocs. [name=Mathieu]
#### En vue
Il existe systématiquement une "box" équivalente en vue.
#### Lien vers des sections
A chaque fois qu'on indique le nom d'une personne, un parcours, un ménage, il y a toujours:
* un lien pour accéder à son dossier (pour autant que l'utilisateur ait les droits d'accès);
* à moins qu'il ne soit indiqué dans une phrase, l'icône de son dossier avant ou après (donc un bonhomme pour la personne, une maison pour le ménage, un fa-random pour les parcours);
Ces éléments sont toujours proposé par des `render_box` par défaut. Des options permettent de les désactiver dans des cas particuliers
> à discuter, quelques réflexion:
> quelle est la logique qui domine pour les boutons ? on a symbolisé les 4 actions du crud par des couleurs: bleu(show) orange(edit) vert(create) et rouge(delete).
> Est-ce que c'est ça qui prime, et comment ça s'articule avec la logique des pictos ?
> Par exemple, il pourrait être logique d'utiliser l'oeil bleu pour voir l'objet, qu'il s'agisse d'une personne ou d'un parcours, ce serait plutôt le contexte, et l'infobulle (title) qui préciserait le contexte.
> Je pense que les pictos de boutons doivent faire référence à l'action, mais pas à l'objet. Autrement dit je n'utiliserais jamais l'icone du ménage ou du parcours dans les boutons.
> Pour représenter les ménages et les parcours, je pense qu'il faudrait trouver autre chose que forkawesome. Si c'est des pictos, trouver un motif différents et de tailles différente. Réfléchir à un couplage picto-couleur-forme différent, qui exprime le contexte et qui se distingue bien des boutons.
> Idem pour les badges, il faut une palette de badge qui couvre tous les besoins: socialIssue, socialActions, socialReason, members, etc. [name=Mathieu]
### Formulaires
#### Vocabulaire:
Utiliser toujours:
* `Créer` dans un `bt bt-create` pour les **liens** vers le formulairep pour créer une entité (pour parvenir au formulaire);
* `Enregistrer` dans un `bt bt-save` pour les boutons "Enregistrer" (dans un formulaire édition **ou** création);
* `Enregistrer et nouveau`
* `Enregistrer et voir`
* `Modifier` dans un `bt bt-edit` pour les **liens** vers le formulaire d'édition
* `Dupliquer` (préciser là où on peut le voir)
* `Annuler` pour quitter une page d'édition avec un lien vers la liste, ou le `returnPath`
#### Retour après un enregistrement
Après avoir cliqué sur "Créer" ou "Sauver", la page devrait revenir:
* vers le returnPath, s'il existe;
* sinon, vers la page "vue".
### Bandeaux contenant les boutons d'actions
Les boutons sont toujours dans un bandeau "sticky-form" dans le bas du formulaire ou de la page de liste.
Si pertinent:
* Le bandeau contient un bouton "Annuler" qui retourne à la page précédente. Il est obligatoire pour les formulaires, optionnel pour les listes ou les pages "résumés"
* Ce bouton "annuler" est toujours à gauche
```
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_entity_return_path('route_name' { 'route': 'option' } )}}">{{ return_path_label }}</a>
</li>
<li>
<!-- action 1 -->
</li>
</ul>
```
### Messages flash
#### A la création d'une entité
A chaque fois qu'un élément est créé par un formulaire, un message flash doit apparaitre. Il indique:
> "L'élément a été créé"
Le nom de l'élément peut être remplacé par quelque chose de plus pertinent:
> * L'activité a été créée
> * Le rendez-vous a été créé
> * ...
#### A l'enregistrement d'une entité
A chaque fois qu'un élément est enregistré, un message flash doit apparaitre:
> * Les données ont été modifiées
>
#### Erreur sur un formulaire (erreur de validation)
En tête d'un formulaire, un message flash doit indiquer que des validations n'ont pas réussi:
> Ce formulaire contient des erreurs
Les erreurs doivent apparaitre attachée au champ qui les concerne. Toutefois, il est acceptable d'afficher les erreurs à la racine du formulaire s'il était complexe, techniquement, d'attacher les erreurs.
### Liens de retour
A chaque fois qu'un lien est indiqué, vérifier si on ne doit pas utiliser la fonction `chill_return_path`, `chill_forward_return_path` ou `chill_return_path_or`.
* depuis la page liste, vers l'ouverture d'un élément, ou le bouton création => utiliser `chill_path_add_return_path`
* dans ces pages d'éditions,
* utiliser `chill_return_path_or` dans le bouton "Cancel";
* pour les boutons "enregistrer et voir" et "Enregistrer et fermer" => ?

View File

@@ -27,3 +27,4 @@ Version 1.5.5
=============
- [activity] replace dropdown for selecting reasons and use chillEntity for reason rendering
- fix bug: error when trying to edit activity of which the type has been deactivated

View File

@@ -174,6 +174,10 @@ class ActivityController extends AbstractController
$activityType = $em->getRepository(\Chill\ActivityBundle\Entity\ActivityType::class)
->find($activityType_id);
if (isset($activityType) && !$activityType->isActive()) {
throw new \InvalidArgumentException('Activity type must be active');
}
$activityData = null;
if ($request->query->has('activityData')) {
$activityData = $request->query->get('activityData');

View File

@@ -141,7 +141,6 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface, C
$ref = 'activity_'.$person->getFullnameCanonical();
for($i = 0; $i < $activityNbr; $i ++) {
print "Creating an activity type for : ".$person." (ref: ".$ref.") \n";
$activity = $this->newRandomActivity($person);
$manager->persist($activity);
}

View File

@@ -93,15 +93,13 @@ class ActivityType extends AbstractType
/** @var \Chill\ActivityBundle\Entity\ActivityType $activityType */
$activityType = $options['activityType'];
if (!$activityType->isActive()) {
throw new \InvalidArgumentException('Activity type must be active');
}
// TODO revoir la gestion des center au niveau du form des activité.
if ($options['center']) {
$builder->add('scope', ScopePickerType::class, [
'center' => $options['center'],
'role' => $options['role']
'role' => $options['role'],
// TODO make required again once scope and rights are fixed
'required' => false
]);
}

View File

@@ -1,7 +1,9 @@
// Access to Bootstrap variables and mixins
@import '~ChillMainAssets/module/bootstrap/shared';
// activity creation first step: select type page
//// ACTIVITY CREATION
// first step: select type page
div.new-activity-select-type {
div.activity-row {
display: flex;
@@ -21,91 +23,36 @@ div.new-activity-select-type {
}
}
// exceptions for flex-table in list-records
div.activity-list {
div.flex-table {
div.item-bloc {
div.item-row.main {
div.item-col {
&:first-child {
flex-basis: 15%;
}
ul.list-content {
li.social-issues, li.social-actions {
.badge-primary {
font-variant: small-caps;
font-weight: bold;
font-size: 88%;
margin-bottom: 0.2em;
}
}
li.social-issues .badge-primary {
background-color: $orange;
}
li.social-actions .badge-primary {
background-color: $green;
}
}
}
}
div.item-row.comment {
margin-left: 15%;
blockquote.chill-user-quote {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
}
div.item-row.details {
margin-left: 15%;
//// ACTIVITY LIST PAGE
// precise badge-title specific details
// override flex-bloc to adapt in list
// TODO refund this
div.accompanyingCourse.flex-bloc.concerned-groups {
margin: 0;
width: 100%;
justify-content: space-around;
div.item-bloc {
box-shadow: unset;
padding: 0;
flex-basis: 25%;
div.item-row {
flex-direction: column;
div.item-col {
&:first-child {
width: unset;
}
&:last-child {
border-top: 0;
margin-top: 0;
padding-top: 0;
ul.list-content {
padding: 0;
}
}
}
}
}
h2.badge-title {
div.duration {
font-size: smaller;
padding-left: 1em;
margin-top: 1em;
}
ul.list-content {
font-size: 70%;
list-style-type: none;
padding-left: 0;
margin: 0;
li {
margin-bottom: 0.2em;
// exception: change bg color for action badges above badge-title
.bg-light {
background-color: $chill-light-gray !important;
}
}
ul.list-content {
list-style-type: none;
padding-left: 1em;
margin: 0 0;
li {
margin-bottom: 0.2em;
}
}
}
div.duration {
font-size: smaller;
padding-left: 1em;
margin-top: 1em;
}
}
}
}
}
div.main {
padding: 1em;
}
// exceptions for flex-bloc in concerned-groups
//// ACTIVITY SHOW AND FORM PAGES
// Exceptions for flex-bloc in concerned-groups
div.flex-bloc.concerned-groups {
margin-top: 1em;
div.item-bloc {
@@ -130,7 +77,6 @@ div.flex-bloc.concerned-groups {
}
}
/// CHILL ENTITY RENDER BOX
.chill-entity {

View File

@@ -0,0 +1,88 @@
<h2 class="badge-title">
<span class="title_label">
{% if activity.date %}
<h3>{{ activity.date|format_date('short') }}</h3>
{% endif %}
<div class="duration">
{% if activity.durationTime and t.durationTimeVisible %}
<p>
<abbr class="fa fa-fw fa-hourglass-end" title="{{ 'Duration Time'|trans }}"></abbr>
{{ activity.durationTime|date('H:i') }}
</p>
{% endif %}
{% if activity.travelTime and t.travelTimeVisible %}
<p>
<abbr class="fa fa-fw fa-car" title="{{ 'Travel time'|trans }}"></abbr>
{{ activity.travelTime|date('H:i') }}
</p>
{% endif %}
</div>
</span>
<span class="title_action">
{{ activity.type.name | localize_translatable_string }}
<ul class="small_in_title">
<li>
<abbr title="{{ 'location'|trans }}">{{ 'location'|trans ~ ': ' }}</abbr>
{# TODO {% if activity.location %}{{ activity.location }}{% endif %} #}
Domicile de l'usager
</li>
{% if activity.user and t.userVisible %}
<li>
<abbr title="{{ 'Referrer'|trans }}">{{ 'Referrer'|trans ~ ': ' }}</abbr>
{{ activity.user.usernameCanonical }}
</li>
{% endif %}
</ul>
<ul class="list-content">
{%- if t.reasonsVisible -%}
{%- if activity.reasons is not empty -%}
<li class="reasons">
{% for r in activity.reasons %}
{{ r|chill_entity_render_box }}
{% endfor %}
</li>
{%- endif -%}
{% endif %}
{%- if t.socialIssuesVisible %}
{%- if activity.socialIssues is not empty -%}
<li class="social-issues">
{% for r in activity.socialIssues %}
{{ r|chill_entity_render_box }}
{% endfor %}
</li>
{%- endif -%}
{% endif %}
{%- if t.socialActionsVisible -%}
{%- if activity.socialActions is not empty -%}
<li class="social-actions">
{% for r in activity.socialActions %}
{{ r|chill_entity_render_box }}
{% endfor %}
</li>
{%- endif -%}
{% endif %}
</ul>
</span>
</h2>
{#
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
<div class="mt-3">
<a class="btn btn-sm btn-outline-primary"
title="{{ 'Period number %number%'|trans({'%number%': activity.accompanyingPeriod.id}) }}"
href="{{ chill_path_add_return_path(
"chill_person_accompanying_course_index",
{ 'accompanying_period_id': activity.accompanyingPeriod.id }
) }}"><i class="fa fa-random"></i>
</a>
</div>
{% endif %}
#}

View File

@@ -5,17 +5,17 @@
{% if context == 'person' %}
{% set blocs = [
{ 'title': 'Others persons'|trans,
{ 'title': 'Others persons'|trans,
'items': entity.persons,
'path' : 'chill_person_view',
'key' : 'person_id'
},
{ 'title': 'Third parties'|trans,
{ 'title': 'Third parties'|trans,
'items': entity.thirdParties,
'path' : 'chill_3party_3party_show',
'key' : 'thirdparty_id'
},
{ 'title': 'Users concerned'|trans,
{ 'title': 'Users concerned'|trans,
'items': entity.users,
'path' : 'admin_user_show',
'key' : 'id'
@@ -23,22 +23,22 @@
] %}
{% else %}
{% set blocs = [
{ 'title': 'Persons in accompanying course'|trans,
{ 'title': 'Persons in accompanying course'|trans,
'items': entity.personsAssociated,
'path' : 'chill_person_view',
'key' : 'person_id'
},
{ 'title': 'Third persons'|trans,
{ 'title': 'Third persons'|trans,
'items': entity.personsNotAssociated,
'path' : 'chill_person_view',
'key' : 'person_id'
},
{ 'title': 'Third parties'|trans,
{ 'title': 'Third parties'|trans,
'items': entity.thirdParties,
'path' : 'chill_3party_3party_show',
'key' : 'thirdparty_id'
},
{ 'title': 'Users concerned'|trans,
{ 'title': 'Users concerned'|trans,
'items': entity.users,
'path' : 'admin_user_show',
'key' : 'id'
@@ -101,3 +101,31 @@
{% endfor %}
</div>
{% endif %}
{% if (with_display == 'wrap-list') %}
<div class="concerned-groups wrap-list">
{% for bloc in blocs %}
<div class="wl-row">
{% if bloc.items|length > 0 %}
<div class="wl-col title">
<h4>{{ bloc.title }}</h4>
</div>
<div class="wl-col list">
{% for item in bloc.items %}
<span class="wl-item {% if (badge_person is defined and badge_person == true) %}badge-person{% else %}badge bg-primary{% endif %}">
<a href="{{ _self.href(bloc.path, bloc.key, item.id) }}">
{{ item|chill_entity_render_box({
'render': 'raw',
'addAltNames': false
}) }}
</a>
</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}

View File

@@ -1,4 +1,4 @@
<div class="activity-list context-{{ context }}">
<div class="context-{{ context }}">
{% if activities|length == 0 %}
<p class="chill-no-data-statement">
@@ -7,176 +7,56 @@
</p>
{% else %}
<div class="flex-table">
<div class="flex-table activity-list">
{% for activity in activities %}
{% set t = activity.type %}
<div class="item-bloc">
<div class="item-row main">
<div class="item-col">
{% if activity.date %}
<h3>{{ activity.date|format_date('long') }}</h3>
{% endif %}
<div class="duration">
{% if activity.durationTime and t.durationTimeVisible %}
<p>
<i class="fa fa-fw fa-hourglass-end"></i>
{{ activity.durationTime|date('H:i') }}
</p>
{% endif %}
{% if activity.travelTime and t.travelTimeVisible %}
<p>
<i class="fa fa-fw fa-car"></i>
{{ activity.travelTime|date('H:i') }}
</p>
{% endif %}
</div>
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
<div class="mt-3">
<a class="btn btn-sm btn-outline-primary"
title="{{ 'Period number %number%'|trans({'%number%': activity.accompanyingPeriod.id}) }}"
href="{{ chill_path_add_return_path(
"chill_person_accompanying_course_index",
{ 'accompanying_period_id': activity.accompanyingPeriod.id }
) }}"><i class="fa fa-random"></i>
</a>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="float-button top">
<div class="box">
<div class="action">
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activity_show', { 'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}"
class="btn btn-sm btn-show "></a>
</li>
{% if no_action is not defined or no_action == false %}
{# TODO
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
#}
<li>
<a href="{{ path('chill_activity_activity_edit', { 'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}"
class="btn btn-sm btn-update "></a>
</li>
{# TODO
{% endif %}
{% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
#}
<li>
<a href="{{ path('chill_activity_activity_delete', { 'id': activity.id, 'person_id' : person_id, 'accompanying_period_id': accompanying_course_id } ) }}"
class="btn btn-sm btn-delete "></a>
</li>
{# TODO
{% endif %}
#}
{% endif %}
</ul>
</div>
<ul class="list-content">
{% if activity.user and t.userVisible %}
<li>
<abbr class="referrer" title="{{ 'Referrer'|trans }}">ref:</abbr>
<b>{{ activity.user.usernameCanonical }}</b>
</li>
{% endif %}
<li>
<b>{{ activity.type.name | localize_translatable_string }}</b>
{% if activity.attendee is not null and t.attendeeVisible %}
{% if activity.attendee %}
{{ '→ ' ~ 'present'|trans|capitalize }}
{% else %}
{{ '→ ' ~ 'not present'|trans|capitalize }}
{% endif %}
{% endif %}
</li>
<li>
<b>{{ 'location'|trans ~ ': ' }}</b>
Domicile de l'usager
{# TODO {% if activity.location %}{{ activity.location }}{% endif %}
#}
</li>
{%- if t.reasonsVisible -%}
<li>
{%- if activity.reasons is not empty -%}
{% for r in activity.reasons %}
{{ r|chill_entity_render_box }}
{% endfor %}
{%- endif -%}
</li>
{% endif %}
{%- if t.socialIssuesVisible %}
<li class="social-issues">
{%- if activity.socialIssues is not empty -%}
{% for r in activity.socialIssues %}
{{ r|chill_entity_render_box }}
{% endfor %}
{%- endif -%}
</li>
{% endif %}
{%- if t.socialActionsVisible -%}
<li class="social-actions">
{%- if activity.socialActions is not empty -%}
{% for r in activity.socialActions %}
{{ r|chill_entity_render_box }}
{% endfor %}
{%- endif -%}
</li>
{% endif %}
</ul>
</div>
</div>
</div>
<div class="item-row">
{% include '@ChillActivity/Activity/activity-badge-title.html.twig' %}
</div>
{% if activity.comment.comment is not empty
or activity.persons|length > 0
or activity.thirdParties|length > 0
or activity.users|length > 0
%}
<div class="item-row comment separator">
{% if activity.comment.comment is not empty %}
{{ activity.comment|chill_entity_render_box({
'disable_markdown': false,
'limit_lines': 3,
'metadata': false,
}) }}
{% endif %}
</div>
<div class="item-row details">
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
'context': context,
'with_display': 'bloc',
'entity': activity,
'badge_person': true
} %}
</div>
<div class="main">
{% if activity.comment.comment is not empty %}
{{ activity.comment|chill_entity_render_box({
'disable_markdown': false,
'limit_lines': 3,
'metadata': false,
}) }}
{% endif %}
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
'context': context,
'with_display': 'row',
'entity': activity,
'badge_person': true
} %}
</div>
{% endif %}
<div class="item-row separator">
<div class="updatedBy"></div>
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activity_show', { 'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}"
class="btn btn-sm btn-show "></a>
</li>
{% if no_action is not defined or no_action == false %}
<li>
<a href="{{ path('chill_activity_activity_edit', { 'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}"
class="btn btn-sm btn-update "></a>
</li>
<li>
<a href="{{ path('chill_activity_activity_delete', { 'id': activity.id, 'person_id' : person_id, 'accompanying_period_id': accompanying_course_id } ) }}"
class="btn btn-sm btn-delete "></a>
</li>
{% endif %}
</ul>
</div>
</div>
{% endfor %}

View File

@@ -0,0 +1,10 @@
<div class="activity-list">
{% for activity in activities | slice(0,5) %}
{% set t = activity.type %}
<a href="{{ path('chill_activity_activity_show', { 'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}"></a>
{% include '@ChillActivity/Activity/activity-badge-title.html.twig' %}
{% endfor %}
</div>

View File

@@ -73,7 +73,7 @@
{% endif %}
{% if t.travelTimeVisible %}
<dt class="inline">{{ 'Travel Time'|trans }}</dt>
<dt class="inline">{{ 'Travel time'|trans }}</dt>
<dd>{% if entity.travelTime is not null %}
{{ entity.travelTime|date('H:i') }}
{% else %}

View File

@@ -210,3 +210,5 @@ Group by reasons: Sujet d'activité
Aggregate by activity user: Aggréger par utilisateur lié à l'activité
Aggregate by activity type: Aggréger par type d'activité
Aggregate by activity reason: Aggréger par sujet de l'activité
Last activities: Les dernières activités

View File

@@ -7,6 +7,7 @@ namespace Chill\AsideActivityBundle\Controller;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Chill\AsideActivityBundle\Repository\AsideActivityCategoryRepository;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Pagination\PaginatorInterface;
@@ -22,9 +23,9 @@ final class AsideActivityController extends CRUDController
$this->categoryRepository = $categoryRepository;
}
protected function buildQueryEntities(string $action, Request $request)
protected function buildQueryEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null)
{
$qb = parent::buildQueryEntities($action, $request);
$qb = parent::buildQueryEntities($action, $request, $filterOrder);
if ('index' === $action) {
$qb->where($qb->expr()->eq('e.agent', ':user'));
@@ -53,7 +54,7 @@ final class AsideActivityController extends CRUDController
$asideActivity = new AsideActivity();
$duration = $request->query->get('duration', '300');
$duration = \DateTime::createFromFormat('U', $duration);
$duration = \DateTime::createFromFormat('U', $duration);
$asideActivity->setDuration($duration);
$categoryId = $request->query->get('type', 7);

View File

@@ -1,16 +1,16 @@
{#
* 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/>.
#}
@@ -33,7 +33,7 @@
{# CFChoice : render the different elements in a choice list #}
{% block cf_choices_row %}
<h3>{{ 'Choices'|trans }}</h3>
<div id="{{ form.vars.id }}" data-prototype="{{- form_row(form.vars.prototype.children.name)
~ form_row(form.vars.prototype.children.active)
~ form_row(form.vars.prototype.children.slug) -}}">
@@ -47,8 +47,8 @@
{% endfor %}
</tbody></table>
</div>
{# we use javascrit to add an additional element. All functions are personnalized with the id ( = form.vars.id) #}
<script type="text/javascript">
function addElementInDiv(div_id) {
@@ -109,3 +109,13 @@
{# The choice_with_other_widget widget is defined in the main bundle #}
{% block pick_address_row %}
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
{% endblock %}
{% block pick_address_widget %}
{{ form_widget(form) }}
<div data-input-address-container="{{ form.vars.uniqid }}"></div>
{% endblock %}

View File

@@ -20,6 +20,8 @@
namespace Chill\MainBundle\CRUD\Controller;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -209,22 +211,24 @@ class CRUDController extends AbstractController
* This method:
*
* 1. Launch `onPreIndex`
* x. check acl. If it does return a response instance, return it
* x. launch `onPostCheckACL`. If it does return a response instance, return it
* 1. count the items, using `countEntities`
* 2. build a paginator element from the the number of entities ;
* 3. Launch `onPreIndexQuery`. If it does return a response instance, return it
* 3. build a query, using `queryEntities`
* x. fetch the results, using `getQueryResult`
* x. Launch `onPostIndexFetchQuery`. If it does return a response instance, return it
* 4. create default parameters:
* 2. check acl. If it does return a response instance, return it
* 3. launch `onPostCheckACL`. If it does return a response instance, return it
* 4. count the items, using `countEntities`
* 5. build a paginator element from the the number of entities ;
* 6. Launch `onPreIndexQuery`. If it does return a response instance, return it
* 7. fetch the results, using `getQueryResult`
*
* Internally, this build a query, using `queryEntities`
*
* 8. Launch `onPostIndexFetchQuery`. If it does return a response instance, return it
* 9. create default parameters:
*
* The default parameters are:
*
* * entities: the list en entities ;
* * crud_name: the name of the crud ;
* * paginator: a paginator element ;
* 5. Launch rendering, the parameter is fetch using `getTemplateFor`
* 10. Launch rendering, the parameter is fetch using `getTemplateFor`
* The parameters may be personnalized using `generateTemplateParameter`.
*
* @param string $action
@@ -249,7 +253,8 @@ class CRUDController extends AbstractController
return $response;
}
$totalItems = $this->countEntities($action, $request);
$filterOrder = $this->buildFilterOrderHelper($action, $request);
$totalItems = $this->countEntities($action, $request, $filterOrder);
$paginator = $this->getPaginatorFactory()->create($totalItems);
$response = $this->onPreIndexBuildQuery($action, $request, $totalItems,
@@ -259,16 +264,7 @@ class CRUDController extends AbstractController
return $response;
}
$query = $this->queryEntities($action, $request, $paginator);
$response = $this->onPostIndexBuildQuery($action, $request, $totalItems,
$paginator, $query);
if ($response instanceof Response) {
return $response;
}
$entities = $this->getQueryResult($action, $request, $totalItems, $paginator, $query);
$entities = $this->getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
$response = $this->onPostIndexFetchQuery($action, $request, $totalItems,
$paginator, $entities);
@@ -280,7 +276,8 @@ class CRUDController extends AbstractController
$defaultTemplateParameters = [
'entities' => $entities,
'crud_name' => $this->getCrudName(),
'paginator' => $paginator
'paginator' => $paginator,
'filter_order' => $filterOrder
];
return $this->render(
@@ -289,6 +286,11 @@ class CRUDController extends AbstractController
);
}
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
{
return null;
}
/**
* @param string $action
* @param Request $request
@@ -361,9 +363,9 @@ class CRUDController extends AbstractController
* @param PaginatorInterface $paginator
* @return type
*/
protected function queryEntities(string $action, Request $request, PaginatorInterface $paginator)
protected function queryEntities(string $action, Request $request, PaginatorInterface $paginator, ?FilterOrderHelper $filterOrder = null)
{
$query = $this->buildQueryEntities($action, $request)
$query = $this->buildQueryEntities($action, $request, $filterOrder)
->setFirstResult($paginator->getCurrentPage()->getFirstItemNumber())
->setMaxResults($paginator->getItemsPerPage());
@@ -393,11 +395,13 @@ class CRUDController extends AbstractController
* @param Request $request
* @param int $totalItems
* @param PaginatorInterface $paginator
* @param mixed $query
* @return mixed
*/
protected function getQueryResult(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, $query)
protected function getQueryResult(string $action, Request $request, int $totalItems, PaginatorInterface $paginator,
?FilterOrderHelper $filterOrder = null)
{
$query = $this->queryEntities($action, $request, $paginator, $filterOrder);
return $query->getQuery()->getResult();
}
@@ -408,9 +412,9 @@ class CRUDController extends AbstractController
* @param Request $request
* @return int
*/
protected function countEntities(string $action, Request $request): int
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
{
return $this->buildQueryEntities($action, $request)
return $this->buildQueryEntities($action, $request, $filterOrder)
->select('COUNT(e)')
->getQuery()
->getSingleScalarResult()
@@ -1183,6 +1187,11 @@ class CRUDController extends AbstractController
return $this->get(Resolver::class);
}
protected function getFilterOrderHelperFactory(): FilterOrderHelperFactoryInterface
{
return $this->get(FilterOrderHelperFactoryInterface::class);
}
/**
* @return array
*/
@@ -1197,6 +1206,7 @@ class CRUDController extends AbstractController
EventDispatcherInterface::class => EventDispatcherInterface::class,
Resolver::class => Resolver::class,
SerializerInterface::class => SerializerInterface::class,
FilterOrderHelperFactoryInterface::class => FilterOrderHelperFactoryInterface::class,
]
);
}

View File

@@ -19,6 +19,8 @@
namespace Chill\MainBundle\Command;
use Chill\MainBundle\Doctrine\Model\Point;
use Chill\MainBundle\Entity\Country;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -69,6 +71,9 @@ class LoadPostalCodesCommand extends Command
. "using the postal code and name. \n"
. "The CSV file must have the following columns: "
. "postal code, label, country code."
. "Optionally, the csv file can have the following "
. "columns after the country code: reference code, latitude, longitude, source. "
. "The latitude and longitude columns are supposed to be in WGS84 and expressed in decimal degrees. "
. "The CSV file should not have any header row.")
->addArgument('csv_file', InputArgument::REQUIRED, "the path to "
. "the csv file. See the help for specifications.")
@@ -163,7 +168,7 @@ class LoadPostalCodesCommand extends Command
}
$em = $this->entityManager;
$country = $em
->getRepository('ChillMainBundle:Country')
->getRepository(Country::class)
->findOneBy(array('countryCode' => $row[2]));
if ($country === NULL) {
@@ -173,7 +178,7 @@ class LoadPostalCodesCommand extends Command
// try to find an existing postal code
$existingPC = $em
->getRepository('ChillMainBundle:PostalCode')
->getRepository(PostalCode::class)
->findBy(array('code' => $row[0], 'name' => $row[1]));
if (count($existingPC) > 0) {
@@ -187,6 +192,18 @@ class LoadPostalCodesCommand extends Command
->setCountry($country)
;
if (NULL != $row[3]){
$postalCode->setRefPostalCodeId($row[3]);
}
if (NULL != $row[4] & NULL != $row[5]){
$postalCode->setCenter(Point::fromLonLat((float) $row[5], (float) $row[4]));
}
if (NULL != $row[6]){
$postalCode->setPostalCodeSource($row[6]);
}
$errors = $this->validator->validate($postalCode);
if ($errors->count() == 0) {

View File

@@ -0,0 +1,41 @@
<?php
namespace Chill\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\Civility;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
class LoadCivility extends Fixture implements FixtureGroupInterface
{
public static function getGroups(): array
{
return ['civilities'];
}
public function load(ObjectManager $manager): void
{
$civilities = [
['name' => ['fr' => "Monsieur" ], 'abbrev' => ['fr' => 'M.']],
['name' => ['fr' => "Madame" ], 'abbrev' => ['fr' => 'Mme']],
['name' => ['fr' => "Docteur" ], 'abbrev' => ['fr' => 'Dr']],
['name' => ['fr' => "Professeur" ], 'abbrev' => ['fr' => 'Pr']],
['name' => ['fr' => "Madame la Directrice" ], 'abbrev' => ['fr' => 'Mme']],
['name' => ['fr' => "Monsieur le Directeur" ], 'abbrev' => ['fr' => 'M.']],
['name' => ['fr' => "Madame la Maire" ]],
['name' => ['fr' => "Monsieur le Maire" ]],
['name' => ['fr' => "Maître" ], 'abbrev' => ['fr' => 'Me']],
];
foreach ( $civilities as $val) {
$civility = (new Civility())
->setName($val['name'])
->setAbbreviation($val['abbrev'] ?? [])
->setActive(true);
$manager->persist($civility);
}
$manager->flush();
}
}

View File

@@ -22,6 +22,8 @@
namespace Chill\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Doctrine\Model\Point;
use Chill\MainBundle\Entity\Country;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
@@ -40,21 +42,41 @@ class LoadPostalCodes extends AbstractFixture implements OrderedFixtureInterface
return 50;
}
public static $refs = array();
public static $refs = [];
public function load(ObjectManager $manager)
{
$lines = str_getcsv(self::$codes, "\n");
$belgium = $manager->getRepository('ChillMainBundle:Country')
->findOneBy(array('countryCode' => 'BE'));
echo "loading postal codes... \n";
$this->loadPostalCodeCSV($manager, self::$postalCodeBelgium, 'BE');
$this->loadPostalCodeCSV($manager, self::$postalCodeFrance, 'FR');
}
private function loadPostalCodeCSV(ObjectManager $manager, string $csv, string $countryCode) {
$lines = str_getcsv($csv, "\n");
$country = $manager->getRepository(Country::class)
->findOneBy(['countryCode' => $countryCode]);
foreach($lines as $line) {
$code = str_getcsv($line);
$c = new PostalCode();
$c->setCountry($belgium)
$c->setCountry($country)
->setCode($code[0])
->setName(\ucwords(\strtolower($code[2])))
->setName(\ucwords(\strtolower($code[1])))
;
if (NULL != $code[3]){
$c->setRefPostalCodeId($code[3]);
}
if (NULL != $code[4] & NULL != $code[5]){
$c->setCenter(Point::fromLonLat((float) $code[5], (float) $code[4]));
}
if (NULL != $code[6]){
$c->setPostalCodeSource($code[6]);
}
$manager->persist($c);
$ref = 'postal_code_'.$code[0];
@@ -66,40 +88,342 @@ class LoadPostalCodes extends AbstractFixture implements OrderedFixtureInterface
$manager->flush();
}
private static $codes = <<<EOF
1000,BRUXELLES,BRUXELLES,Bruxelles
1020,Laeken,BRUXELLES,Bruxelles
1030,SCHAERBEEK,SCHAERBEEK,Bruxelles
1040,ETTERBEEK,ETTERBEEK,Bruxelles
1050,IXELLES,IXELLES,Bruxelles
1060,SAINT-GILLES,SAINT-GILLES,Bruxelles
1070,ANDERLECHT,ANDERLECHT,Bruxelles
1080,MOLENBEEK-SAINT-JEAN,MOLENBEEK-SAINT-JEAN,Bruxelles
1081,KOEKELBERG,KOEKELBERG,Bruxelles
1082,BERCHEM-SAINTE-AGATHE,BERCHEM-SAINTE-AGATHE,Bruxelles
1083,GANSHOREN,GANSHOREN,Bruxelles
1090,JETTE,JETTE,Bruxelles
1120,Neder-Over-Heembeek,BRUXELLES,Bruxelles
1130,Haren,BRUXELLES,Bruxelles
1140,EVERE,EVERE,Bruxelles
1150,WOLUWE-SAINT-PIERRE,WOLUWE-SAINT-PIERRE,Bruxelles
1160,AUDERGHEM,AUDERGHEM,Bruxelles
1170,WATERMAEL-BOITSFORT,WATERMAEL-BOITSFORT,Bruxelles
1180,UCCLE,UCCLE,Bruxelles
1190,FOREST,FOREST,Bruxelles
1200,WOLUWE-SAINT-LAMBERT,WOLUWE-SAINT-LAMBERT,Bruxelles
1210,SAINT-JOSSE-TEN-NOODE,SAINT-JOSSE-TEN-NOODE,Bruxelles
1300,Limal,WAVRE,Brabant-Wallon
1300,WAVRE,WAVRE,Brabant-Wallon
1301,Bierges,WAVRE,Brabant-Wallon
1310,LA HULPE,LA HULPE,Brabant-Wallon
1315,Glimes,INCOURT,Brabant-Wallon
1315,INCOURT,INCOURT,Brabant-Wallon
1315,Opprebais,INCOURT,Brabant-Wallon
1315,Piètrebais,INCOURT,Brabant-Wallon
1315,Roux-Miroir,INCOURT,Brabant-Wallon
1320,BEAUVECHAIN,BEAUVECHAIN,Brabant-Wallon
private static $postalCodeBelgium = <<<EOF
1000,BRUXELLES,BE
1020,BRUXELLES,BE
1030,SCHAERBEEK,BE
1040,ETTERBEEK,BE
1050,IXELLES,BE
1060,SAINT-GILLES,BE
1070,ANDERLECHT,BE
1080,MOLENBEEK-SAINT-JEAN,BE
1081,KOEKELBERG,BE
1082,BERCHEM-SAINTE-AGATHE,BE
1083,GANSHOREN,BE
1090,JETTE,BE
1120,BRUXELLES,BE
1130,BRUXELLES,BE
1140,EVERE,BE
1150,WOLUWE-SAINT-PIERRE,BE
1160,AUDERGHEM,BE
1170,WATERMAEL-BOITSFORT,BE
1180,UCCLE,BE
1190,FOREST,BE
1200,WOLUWE-SAINT-LAMBERT,BE
1210,SAINT-JOSSE-TEN-NOODE,BE
1300,WAVRE,BE
1300,WAVRE,BE
1301,WAVRE,BE
1310,LA HULPE,BE
1315,INCOURT,BE
1315,INCOURT,BE
1315,INCOURT,BE
1315,INCOURT,BE
1315,INCOURT,BE
1320,BEAUVECHAIN,BE
EOF;
private static $postalCodeFrance = <<<EOF
85000,LA ROCHE SUR YON,FR,85191,46.6675261644,-1.4077954093,INSEE
85000,MOUILLERON LE CAPTIF,FR,85155,46.7104764993,-1.46129661418,INSEE
85100,LES SABLES D OLONNE,FR,85194,46.5007612799,-1.79255128677,INSEE
85110,STE CECILE,FR,85202,46.7309688326,-1.12107316048,INSEE
85110,LA JAUDONNIERE,FR,85115,46.6488761831,-0.962477574588,INSEE
85110,ST GERMAIN DE PRINCAY,FR,85220,46.7356314659,-1.04299885081,INSEE
85110,MONSIREIGNE,FR,85145,46.7383480028,-0.931953130855,INSEE
85110,ST VINCENT STERLANGES,FR,85276,46.7397220689,-1.08371759277,INSEE
85110,SIGOURNAIS,FR,85282,46.7140097406,-0.98747730882,INSEE
85110,CHANTONNAY,FR,85051,46.6690167793,-1.04372588019,INSEE
85110,CHANTONNAY,FR,85051,46.6690167793,-1.04372588019,INSEE
85110,ST PROUANT,FR,85266,46.7502017421,-0.974504061491,INSEE
85120,LA CHAPELLE AUX LYS,FR,85053,46.6221916887,-0.642706103195,INSEE
85120,VOUVANT,FR,85305,46.5626835135,-0.764380170382,INSEE
85120,ANTIGNY,FR,85005,46.6191770822,-0.767030058653,INSEE
85120,ST MAURICE DES NOUES,FR,85251,46.5955876221,-0.724327725888,INSEE
85120,LOGE FOUGEREUSE,FR,85125,46.6180854641,-0.6899276733,INSEE
85120,LA TARDIERE,FR,85289,46.663737434,-0.727562430214,INSEE
85120,LA CHATAIGNERAIE,FR,85059,46.6416143401,-0.739561966419,INSEE
85120,ST HILAIRE DE VOUST,FR,85229,46.5914988312,-0.651486245674,INSEE
85120,BREUIL BARRET,FR,85037,46.6503266235,-0.671948654426,INSEE
85120,ST PIERRE DU CHEMIN,FR,85264,46.6957771744,-0.701777715154,INSEE
85130,LA GAUBRETIERE,FR,85097,46.9345007303,-1.05578200702,INSEE
85130,ST MARTIN DES TILLEULS,FR,85247,46.9711539531,-1.06282621567,INSEE
85130,TIFFAUGES,FR,85293,47.0020573556,-1.09858009203,INSEE
85130,CHANVERRIE,FR,85302,46.9634774521,-0.985340006089,INSEE
85130,BAZOGES EN PAILLERS,FR,85013,46.9213757643,-1.14415666313,INSEE
85130,LES LANDES GENUSSON,FR,85119,46.9663828627,-1.12900644447,INSEE
85130,ST AUBIN DES ORMEAUX,FR,85198,46.9958175597,-1.04216568722,INSEE
85140,ESSARTS EN BOCAGE,FR,85084,46.7806739038,-1.22925967851,INSEE
85140,LA MERLATIERE,FR,85142,46.7557703112,-1.29794577,INSEE
85140,ESSARTS EN BOCAGE,FR,85084,46.7806739038,-1.22925967851,INSEE
85140,ESSARTS EN BOCAGE,FR,85084,46.7806739038,-1.22925967851,INSEE
85140,ST MARTIN DES NOYERS,FR,85246,46.7239461187,-1.20379080965,INSEE
85140,CHAUCHE,FR,85064,46.8282791899,-1.27090860656,INSEE
85140,ESSARTS EN BOCAGE,FR,85084,46.7806739038,-1.22925967851,INSEE
85150,ST MATHURIN,FR,85250,46.5686332748,-1.70787622288,INSEE
85150,MARTINET,FR,85138,46.6620680463,-1.6772013304,INSEE
85150,STE FLAIVE DES LOUPS,FR,85211,46.611019489,-1.58031627863,INSEE
85150,STE FOY,FR,85214,46.5327600427,-1.69243074733,INSEE
85150,ST JULIEN DES LANDES,FR,85236,46.6395925444,-1.7159724914,INSEE
85150,ST GEORGES DE POINTINDOUX,FR,85218,46.6423470977,-1.62881823574,INSEE
85150,LE GIROUARD,FR,85099,46.5726064909,-1.58872487716,INSEE
85150,LANDERONDE,FR,85118,46.6549237031,-1.57351777893,INSEE
85150,LES ACHARDS,FR,85152,46.6163645636,-1.65038156849,INSEE
85150,LES ACHARDS,FR,85152,46.6163645636,-1.65038156849,INSEE
85150,VAIRE,FR,85298,46.6055340621,-1.74863672042,INSEE
85160,ST JEAN DE MONTS,FR,85234,46.8021968737,-2.04839789308,INSEE
85170,BELLEVIGNY,FR,85019,46.7756383534,-1.43313700054,INSEE
85170,LE POIRE SUR VIE,FR,85178,46.769919754,-1.50488626452,INSEE
85170,BEAUFOU,FR,85015,46.8191122027,-1.52479250801,INSEE
85170,DOMPIERRE SUR YON,FR,85081,46.7599858068,-1.37275519417,INSEE
85170,BELLEVIGNY,FR,85019,46.7756383534,-1.43313700054,INSEE
85170,LES LUCS SUR BOULOGNE,FR,85129,46.8527299002,-1.48398928084,INSEE
85170,ST DENIS LA CHEVASSE,FR,85208,46.8325959261,-1.3830312677,INSEE
85180,LES SABLES D OLONNE,FR,85194,46.5007612799,-1.79255128677,INSEE
85190,AIZENAY,FR,85003,46.7384516809,-1.62702889721,INSEE
85190,VENANSAULT,FR,85300,46.685677363,-1.54112129191,INSEE
85190,MACHE,FR,85130,46.771364944,-1.69526445062,INSEE
85190,BEAULIEU SOUS LA ROCHE,FR,85016,46.6872087211,-1.62355064963,INSEE
85190,LA GENETOUZE,FR,85098,46.7244524541,-1.50410719693,INSEE
85200,PISSOTTE,FR,85176,46.5010870694,-0.808352236192,INSEE
85200,AUCHAY SUR VENDEE,FR,85009,46.4474386161,-0.876574265149,INSEE
85200,FONTENAY LE COMTE,FR,85092,46.4563186117,-0.793449510859,INSEE
85200,MERVENT,FR,85143,46.5325327351,-0.748519927998,INSEE
85200,DOIX LES FONTAINES,FR,85080,46.3849492327,-0.806840287485,INSEE
85200,FONTENAY LE COMTE,FR,85092,46.4563186117,-0.793449510859,INSEE
85200,LONGEVES,FR,85126,46.4722105292,-0.858917690239,INSEE
85200,ST MARTIN DE FRAIGNEAU,FR,85244,46.4289052087,-0.758948963227,INSEE
85200,SERIGNE,FR,85281,46.5054321828,-0.848819460581,INSEE
85200,BOURNEAU,FR,85033,46.5476882922,-0.813838020265,INSEE
85200,ST MICHEL LE CLOUCQ,FR,85256,46.4861591475,-0.743056336646,INSEE
85200,AUCHAY SUR VENDEE,FR,85009,46.4474386161,-0.876574265149,INSEE
85200,DOIX LES FONTAINES,FR,85080,46.3849492327,-0.806840287485,INSEE
85200,MONTREUIL,FR,85148,46.3973419593,-0.840846860992,INSEE
85200,L ORBRIE,FR,85167,46.4997145725,-0.77427886573,INSEE
85210,ST JEAN DE BEUGNE,FR,85233,46.5196817523,-1.10826075013,INSEE
85210,ST MARTIN LARS EN STE HERMINE,FR,85248,46.5970244335,-0.976384286709,INSEE
85210,LA REORTHE,FR,85188,46.6113748938,-1.04881036553,INSEE
85210,ST AUBIN LA PLAINE,FR,85199,46.5040293195,-1.06506577005,INSEE
85210,ST JUIRE CHAMPGILLON,FR,85235,46.5882648491,-1.00959676911,INSEE
85210,LA CHAPELLE THEMER,FR,85056,46.5639307793,-0.960376685588,INSEE
85210,ST ETIENNE DE BRILLOUET,FR,85209,46.5138850327,-1.01157660374,INSEE
85210,STE HERMINE,FR,85223,46.5572659953,-1.07210861039,INSEE
85210,THIRE,FR,85290,46.5433098199,-1.00699777534,INSEE
85220,ST MAIXENT SUR VIE,FR,85239,46.7329496925,-1.82595152879,INSEE
85220,LA CHAIZE GIRAUD,FR,85045,46.6476375058,-1.81865076161,INSEE
85220,LANDEVIEILLE,FR,85120,46.6444349925,-1.7854367847,INSEE
85220,L AIGUILLON SUR VIE,FR,85002,46.6706426618,-1.82599992318,INSEE
85220,COEX,FR,85070,46.7078707764,-1.75788339462,INSEE
85220,ST REVEREND,FR,85268,46.7057741864,-1.83155480996,INSEE
85220,APREMONT,FR,85006,46.7572682339,-1.74841313647,INSEE
85220,LA CHAPELLE HERMIER,FR,85054,46.6826679204,-1.72083372442,INSEE
85220,COMMEQUIERS,FR,85071,46.7674752232,-1.82534079642,INSEE
85230,BEAUVOIR SUR MER,FR,85018,46.9086155426,-2.06349351302,INSEE
85230,BOUIN,FR,85029,46.9815930867,-2.00423808381,INSEE
85230,ST URBAIN,FR,85273,46.8818371328,-2.01607828912,INSEE
85230,ST GERVAIS,FR,85221,46.9285711589,-1.98059327519,INSEE
85240,MARILLET,FR,85136,46.5667525381,-0.634287713939,INSEE
85240,ST HILAIRE DES LOGES,FR,85227,46.4747117878,-0.650611151998,INSEE
85240,FAYMOREAU,FR,85087,46.5427361252,-0.624271378946,INSEE
85240,XANTON CHASSENON,FR,85306,46.4519408659,-0.706316598666,INSEE
85240,FOUSSAIS PAYRE,FR,85094,46.5230750581,-0.687135962627,INSEE
85240,RIVES D AUTISE,FR,85162,46.424726987,-0.665995249042,INSEE
85240,PUY DE SERRE,FR,85184,46.5650384637,-0.680144631346,INSEE
85250,ST ANDRE GOULE D OIE,FR,85196,46.8410224478,-1.19644211396,INSEE
85250,LA RABATELIERE,FR,85186,46.8584147661,-1.2569733759,INSEE
85250,CHAVAGNES EN PAILLERS,FR,85065,46.8951394423,-1.24054768713,INSEE
85250,ST FULGENT,FR,85215,46.8705618525,-1.16246465678,INSEE
85250,VENDRENNES,FR,85301,46.8226741756,-1.11650982164,INSEE
85260,LA COPECHAGNIERE,FR,85072,46.8523980181,-1.34349746898,INSEE
85260,L HERBERGEMENT,FR,85108,46.9166207979,-1.37033557148,INSEE
85260,MONTREVERD,FR,85197,46.9277672307,-1.4126154924,INSEE
85260,MONTREVERD,FR,85197,46.9277672307,-1.4126154924,INSEE
85260,MONTREVERD,FR,85197,46.9277672307,-1.4126154924,INSEE
85260,LES BROUZILS,FR,85038,46.8854235235,-1.33186892233,INSEE
85270,NOTRE DAME DE RIEZ,FR,85189,46.7532179022,-1.8935292542,INSEE
85270,ST HILAIRE DE RIEZ,FR,85226,46.7432732188,-1.96439228965,INSEE
85270,ST HILAIRE DE RIEZ,FR,85226,46.7432732188,-1.96439228965,INSEE
85280,LA FERRIERE,FR,85089,46.7215872927,-1.33469332327,INSEE
85290,MORTAGNE SUR SEVRE,FR,85151,46.9910941319,-0.946500033344,INSEE
85290,ST LAURENT SUR SEVRE,FR,85238,46.9506837971,-0.901123752328,INSEE
85300,CHALLANS,FR,85047,46.8354416653,-1.84036683139,INSEE
85300,FROIDFOND,FR,85095,46.8789464367,-1.75511438567,INSEE
85300,SOULLANS,FR,85284,46.7951288466,-1.91392699457,INSEE
85300,LE PERRIER,FR,85172,46.8196487652,-1.97926629071,INSEE
85300,SALLERTAINE,FR,85280,46.8659054157,-1.94894081389,INSEE
85310,LA CHAIZE LE VICOMTE,FR,85046,46.6729533879,-1.29188591019,INSEE
85310,NESMY,FR,85160,46.5921936479,-1.40947698594,INSEE
85310,LA CHAIZE LE VICOMTE,FR,85046,46.6729533879,-1.29188591019,INSEE
85310,RIVES DE L YON,FR,85213,46.605637391,-1.3354497172,INSEE
85310,RIVES DE L YON,FR,85213,46.605637391,-1.3354497172,INSEE
85310,LE TABLIER,FR,85285,46.5596307281,-1.32788759657,INSEE
85320,CHATEAU GUIBERT,FR,85061,46.5741109302,-1.25524886228,INSEE
85320,LES PINEAUX,FR,85175,46.599225902,-1.17865799724,INSEE
85320,ROSNAY,FR,85193,46.5324344973,-1.3007139449,INSEE
85320,BESSAY,FR,85023,46.5397253861,-1.17028433093,INSEE
85320,LA BRETONNIERE LA CLAYE,FR,85036,46.4879459421,-1.26773426545,INSEE
85320,LA BRETONNIERE LA CLAYE,FR,85036,46.4879459421,-1.26773426545,INSEE
85320,CORPE,FR,85073,46.5050234767,-1.17034425311,INSEE
85320,MAREUIL SUR LAY DISSAIS,FR,85135,46.5335825488,-1.22688907859,INSEE
85320,PEAULT,FR,85171,46.502029199,-1.22708559855,INSEE
85320,MAREUIL SUR LAY DISSAIS,FR,85135,46.5335825488,-1.22688907859,INSEE
85320,LA COUTURE,FR,85074,46.523938732,-1.26493227292,INSEE
85320,MOUTIERS SUR LE LAY,FR,85157,46.5651677306,-1.16826489836,INSEE
85320,STE PEXINE,FR,85261,46.5596018797,-1.12406235901,INSEE
85330,NOIRMOUTIER EN L ILE,FR,85163,47.0086655085,-2.26243620205,INSEE
85340,L ILE D OLONNE,FR,85112,46.570163703,-1.7737502368,INSEE
85340,LES SABLES D OLONNE,FR,85194,46.5007612799,-1.79255128677,INSEE
85350,L ILE D YEU,FR,85113,46.7093514816,-2.34712702345,INSEE
85350,L ILE D YEU,FR,85113,46.7093514816,-2.34712702345,INSEE
85360,LA TRANCHE SUR MER,FR,85294,46.3564601605,-1.43136322126,INSEE
85370,MOUZEUIL ST MARTIN,FR,85158,46.4591118412,-0.984449849889,INSEE
85370,NALLIERS,FR,85159,46.4658962703,-1.03958611312,INSEE
85370,LE LANGON,FR,85121,46.4393119039,-0.947017086151,INSEE
85390,BAZOGES EN PAREDS,FR,85014,46.6602005512,-0.914053446792,INSEE
85390,ST MAURICE LE GIRARD,FR,85252,46.6398624578,-0.810875649028,INSEE
85390,TALLUD STE GEMME,FR,85287,46.6949386862,-0.886169517112,INSEE
85390,CHAVAGNES LES REDOUX,FR,85066,46.7101499475,-0.915900131393,INSEE
85390,CHEFFOIS,FR,85067,46.6786935635,-0.782949851125,INSEE
85390,MOUILLERON ST GERMAIN,FR,85154,46.6612700667,-0.846784201071,INSEE
85390,MOUILLERON ST GERMAIN,FR,85154,46.6612700667,-0.846784201071,INSEE
85400,STE GEMME LA PLAINE,FR,85216,46.4732196212,-1.11103084694,INSEE
85400,LAIROUX,FR,85117,46.4496842668,-1.27114022202,INSEE
85400,LUCON,FR,85128,46.4510564854,-1.16449285012,INSEE
85400,LES MAGNILS REIGNIERS,FR,85131,46.4635045649,-1.210635375,INSEE
85400,CHASNAIS,FR,85058,46.4459908481,-1.2385924923,INSEE
85410,ST LAURENT DE LA SALLE,FR,85237,46.5854041653,-0.922177315485,INSEE
85410,LA CAILLERE ST HILAIRE,FR,85040,46.6293907412,-0.933153931505,INSEE
85410,LA CAILLERE ST HILAIRE,FR,85040,46.6293907412,-0.933153931505,INSEE
85410,ST CYR DES GATS,FR,85205,46.572397925,-0.86344873853,INSEE
85410,THOUARSAIS BOUILDROUX,FR,85292,46.6062740621,-0.873461023865,INSEE
85410,CEZAIS,FR,85041,46.5917363748,-0.802618133558,INSEE
85410,ST SULPICE EN PAREDS,FR,85271,46.6130038733,-0.8310839288,INSEE
85420,LE MAZEAU,FR,85139,46.3298580373,-0.672957405035,INSEE
85420,LIEZ,FR,85123,46.3698532376,-0.70502476758,INSEE
85420,BOUILLE COURDAULT,FR,85028,46.3847932448,-0.684917815779,INSEE
85420,DAMVIX,FR,85078,46.32063079,-0.743504259797,INSEE
85420,MAILLE,FR,85132,46.3417503082,-0.787487297301,INSEE
85420,ST PIERRE LE VIEUX,FR,85265,46.4009643491,-0.742816267425,INSEE
85420,ST SIGISMOND,FR,85269,46.3368577973,-0.707293731101,INSEE
85420,MAILLEZAIS,FR,85133,46.3642178261,-0.750260780443,INSEE
85420,RIVES D AUTISE,FR,85162,46.424726987,-0.665995249042,INSEE
85430,AUBIGNY LES CLOUZEAUX,FR,85008,46.6028241769,-1.46743549114,INSEE
85430,NIEUL LE DOLENT,FR,85161,46.5676509922,-1.51560194548,INSEE
85430,AUBIGNY LES CLOUZEAUX,FR,85008,46.6028241769,-1.46743549114,INSEE
85430,LA BOISSIERE DES LANDES,FR,85026,46.5581861734,-1.44371985689,INSEE
85440,ST HILAIRE LA FORET,FR,85231,46.4551155186,-1.53048160541,INSEE
85440,TALMONT ST HILAIRE,FR,85288,46.475786445,-1.62751498166,INSEE
85440,POIROUX,FR,85179,46.5107890457,-1.53929317556,INSEE
85440,GROSBREUIL,FR,85103,46.5390090882,-1.6072005484,INSEE
85440,AVRILLE,FR,85010,46.4744272125,-1.49524360118,INSEE
85440,TALMONT ST HILAIRE,FR,85288,46.475786445,-1.62751498166,INSEE
85450,CHAMPAGNE LES MARAIS,FR,85049,46.3735020647,-1.13380723653,INSEE
85450,LA TAILLEE,FR,85286,46.3852513569,-0.941017792066,INSEE
85450,CHAILLE LES MARAIS,FR,85042,46.3853555319,-1.01044079362,INSEE
85450,CHAILLE LES MARAIS,FR,85042,46.3853555319,-1.01044079362,INSEE
85450,VOUILLE LES MARAIS,FR,85304,46.3891941167,-0.968106001439,INSEE
85450,CHAILLE LES MARAIS,FR,85042,46.3853555319,-1.01044079362,INSEE
85450,MOREILLES,FR,85149,46.4218721314,-1.09404530407,INSEE
85450,PUYRAVAULT,FR,85185,46.3653834101,-1.09115660367,INSEE
85450,STE RADEGONDE DES NOYERS,FR,85267,46.3694246909,-1.06671995264,INSEE
85460,LA FAUTE SUR MER,FR,85307,46.3199919131,-1.31487049579,INSEE
85460,L AIGUILLON SUR MER,FR,85001,46.304138479,-1.2623239198,INSEE
85470,BRETIGNOLLES SUR MER,FR,85035,46.6374826705,-1.86324200464,INSEE
85470,BREM SUR MER,FR,85243,46.6118566989,-1.81003917923,INSEE
85470,BREM SUR MER,FR,85243,46.6118566989,-1.81003917923,INSEE
85480,BOURNEZEAU,FR,85034,46.6296975315,-1.14101742229,INSEE
85480,FOUGERE,FR,85093,46.6617881911,-1.23612691916,INSEE
85480,ST HILAIRE LE VOUHIS,FR,85232,46.6859198669,-1.15222590884,INSEE
85480,THORIGNY,FR,85291,46.6179795025,-1.24888057642,INSEE
85480,BOURNEZEAU,FR,85034,46.6296975315,-1.14101742229,INSEE
85490,BENET,FR,85020,46.368873213,-0.613959918706,INSEE
85490,BENET,FR,85020,46.368873213,-0.613959918706,INSEE
85490,BENET,FR,85020,46.368873213,-0.613959918706,INSEE
85500,BEAUREPAIRE,FR,85017,46.9050210355,-1.10144867013,INSEE
85500,ST PAUL EN PAREDS,FR,85259,46.8303789022,-0.964515191283,INSEE
85500,LES HERBIERS,FR,85109,46.8666125813,-1.02216086186,INSEE
85500,MESNARD LA BAROTIERE,FR,85144,46.851716793,-1.10954466033,INSEE
85500,CHANVERRIE,FR,85302,46.9634774521,-0.985340006089,INSEE
85510,ROCHETREJOUX,FR,85192,46.7852546732,-0.996743019108,INSEE
85510,LE BOUPERE,FR,85031,46.7877960262,-0.930897406714,INSEE
85520,JARD SUR MER,FR,85114,46.4246376808,-1.60014921643,INSEE
85520,ST VINCENT SUR JARD,FR,85278,46.4297504489,-1.54956205778,INSEE
85530,LA BRUFFIERE,FR,85039,47.0148006973,-1.18329758318,INSEE
85540,LE CHAMP ST PERE,FR,85050,46.5157210212,-1.33946630196,INSEE
85540,LE GIVRE,FR,85101,46.4729146043,-1.40256149118,INSEE
85540,MOUTIERS LES MAUXFAITS,FR,85156,46.489279204,-1.43622387207,INSEE
85540,ST VINCENT SUR GRAON,FR,85277,46.5034756656,-1.382954206,INSEE
85540,LA JONCHERE,FR,85116,46.4358647401,-1.38517843312,INSEE
85540,ST BENOIST SUR MER,FR,85201,46.4266286403,-1.3399129604,INSEE
85540,ST VINCENT SUR GRAON,FR,85277,46.5034756656,-1.382954206,INSEE
85540,CURZON,FR,85077,46.4512376923,-1.30138059216,INSEE
85540,ST AVAUGOURD DES LANDES,FR,85200,46.5136722903,-1.47528120789,INSEE
85540,ST CYR EN TALMONDAIS,FR,85206,46.4597334032,-1.33722144355,INSEE
85550,LA BARRE DE MONTS,FR,85012,46.8722784154,-2.09984018879,INSEE
85550,LA BARRE DE MONTS,FR,85012,46.8722784154,-2.09984018879,INSEE
85560,LE BERNARD,FR,85022,46.44832528,-1.43979865314,INSEE
85560,LONGEVILLE SUR MER,FR,85127,46.4091029013,-1.47711855345,INSEE
85570,POUILLE,FR,85181,46.5022256779,-0.955223380119,INSEE
85570,ST VALERIEN,FR,85274,46.5348508899,-0.944470507825,INSEE
85570,L HERMENAULT,FR,85110,46.5156262822,-0.898430664265,INSEE
85570,PETOSSE,FR,85174,46.4796848965,-0.911734176662,INSEE
85570,ST MARTIN DES FONTAINES,FR,85245,46.5464637508,-0.907394581139,INSEE
85570,MARSAIS STE RADEGONDE,FR,85137,46.5380790745,-0.868868309437,INSEE
85580,ST DENIS DU PAYRE,FR,85207,46.4118936776,-1.27222282402,INSEE
85580,ST MICHEL EN L HERM,FR,85255,46.3366903175,-1.2483968538,INSEE
85580,TRIAIZE,FR,85297,46.3792685111,-1.19928351422,INSEE
85580,GRUES,FR,85104,46.3813091348,-1.32364519268,INSEE
85590,TREIZE VENTS,FR,85296,46.9168130123,-0.845959158017,INSEE
85590,LES EPESSES,FR,85082,46.8917166066,-0.903422756546,INSEE
85590,MALLIEVRE,FR,85134,46.9112847287,-0.864977836096,INSEE
85590,ST MALO DU BOIS,FR,85240,46.9248120291,-0.914182961099,INSEE
85590,ST MARS LA REORTHE,FR,85242,46.8597253005,-0.925777900202,INSEE
85600,LA BOISSIERE DE MONTAIGU,FR,85025,46.9451858636,-1.1916392484,INSEE
85600,MONTAIGU VENDEE,FR,85146,46.9759800852,-1.31364530268,INSEE
85600,MONTAIGU VENDEE,FR,85146,46.9759800852,-1.31364530268,INSEE
85600,TREIZE SEPTIERS,FR,85295,46.9975586143,-1.23193361154,INSEE
85600,MONTAIGU VENDEE,FR,85146,46.9759800852,-1.31364530268,INSEE
85600,MONTAIGU VENDEE,FR,85146,46.9759800852,-1.31364530268,INSEE
85600,MONTAIGU VENDEE,FR,85146,46.9759800852,-1.31364530268,INSEE
85610,CUGAND,FR,85076,47.0602388146,-1.25289811103,INSEE
85610,LA BERNARDIERE,FR,85021,47.0361828072,-1.27390355206,INSEE
85620,ROCHESERVIERE,FR,85190,46.9274273036,-1.50132208111,INSEE
85630,BARBATRE,FR,85011,46.9335754783,-2.16743559847,INSEE
85640,MOUCHAMPS,FR,85153,46.7870550926,-1.05454102867,INSEE
85660,ST PHILBERT DE BOUAINE,FR,85262,46.9927907526,-1.5073882242,INSEE
85670,LA CHAPELLE PALLUAU,FR,85055,46.7873638997,-1.62492863273,INSEE
85670,FALLERON,FR,85086,46.8623928354,-1.70108938038,INSEE
85670,ST ETIENNE DU BOIS,FR,85210,46.8418481774,-1.59617737479,INSEE
85670,ST PAUL MONT PENIT,FR,85260,46.8070059547,-1.66964833149,INSEE
85670,ST CHRISTOPHE DU LIGNERON,FR,85204,46.8151386519,-1.74035413493,INSEE
85670,GRAND LANDES,FR,85102,46.8483283063,-1.64453002578,INSEE
85670,PALLUAU,FR,85169,46.8063002019,-1.60225256402,INSEE
85680,LA GUERINIERE,FR,85106,46.9669962053,-2.2302799245,INSEE
85690,NOTRE DAME DE MONTS,FR,85164,46.8424284611,-2.10928732775,INSEE
85700,SEVREMONT,FR,85090,46.8211105864,-0.854584153953,INSEE
85700,LA MEILLERAIE TILLAY,FR,85140,46.742582825,-0.85478763606,INSEE
85700,MONTOURNAIS,FR,85147,46.7502937556,-0.770013941158,INSEE
85700,POUZAUGES,FR,85182,46.7812581702,-0.828778359084,INSEE
85700,REAUMUR,FR,85187,46.7145137269,-0.816742537248,INSEE
85700,SEVREMONT,FR,85090,46.8211105864,-0.854584153953,INSEE
85700,MENOMBLET,FR,85141,46.7301338667,-0.728654955878,INSEE
85700,ST MESMIN,FR,85254,46.8005779435,-0.748892533741,INSEE
85700,SEVREMONT,FR,85090,46.8211105864,-0.854584153953,INSEE
85700,SEVREMONT,FR,85090,46.8211105864,-0.854584153953,INSEE
85710,BOIS DE CENE,FR,85024,46.9479643351,-1.89668693466,INSEE
85710,CHATEAUNEUF,FR,85062,46.916944435,-1.9261131832,INSEE
85710,LA GARNACHE,FR,85096,46.8977541296,-1.82443040539,INSEE
85740,L EPINE,FR,85083,46.9843405667,-2.26449527608,INSEE
85750,ANGLES,FR,85004,46.3870511077,-1.40049386944,INSEE
85770,LES VELLUIRE SUR VENDEE,FR,85177,46.4190919441,-0.910475769222,INSEE
85770,VIX,FR,85303,46.3543018169,-0.856628326667,INSEE
85770,LE GUE DE VELLUIRE,FR,85105,46.3675950645,-0.905432724485,INSEE
85770,L ILE D ELLE,FR,85111,46.3334258655,-0.919100677098,INSEE
85770,LES VELLUIRE SUR VENDEE,FR,85177,46.4190919441,-0.910475769222,INSEE
85800,ST GILLES CROIX DE VIE,FR,85222,46.6904708814,-1.91946363327,INSEE
85800,LE FENOUILLER,FR,85088,46.7161264566,-1.89206667498,INSEE
85800,GIVRAND,FR,85100,46.6822701061,-1.8787272243,INSEE
EOF;
}

View File

@@ -20,33 +20,41 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\ThirdPartyBundle\Entity;
namespace Chill\MainBundle\Entity;
use Chill\ThirdPartyBundle\Repository\ThirdPartyCivilityRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Table(name="chill_3party.party_civility")
* @ORM\Entity(repositoryClass=ThirdPartyCivilityRepository::class)
* @ORM\Table(name="chill_main_civility")
* @ORM\Entity
*/
class ThirdPartyCivility
class Civility
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
*/
private $id;
/**
* @ORM\Column(type="json")
* @Serializer\Groups({"read"})
*/
private $name = [];
private array $name = [];
/**
* @ORM\Column(type="json")
* @Serializer\Groups({"read"})
*/
private array $abbreviation = [];
/**
* @ORM\Column(type="boolean")
*/
private $active = true;
private bool $active = true;
public function getId(): ?int
{
@@ -76,4 +84,22 @@ class ThirdPartyCivility
return $this;
}
/**
* @return array
*/
public function getAbbreviation(): array
{
return $this->abbreviation;
}
/**
* @param array $abbreviation
* @return Civility
*/
public function setAbbreviation(array $abbreviation): self
{
$this->abbreviation = $abbreviation;
return $this;
}
}

View File

@@ -2,6 +2,7 @@
namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Doctrine\Model\Point;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
@@ -62,6 +63,30 @@ class PostalCode
*/
private $origin = 0;
/**
* @var string
*
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"read"})
*/
private $refPostalCodeId;
/**
* @var string
*
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"read"})
*/
private $postalCodeSource;
/**
* @var Point
*
* @ORM\Column(type="point", nullable=true)
* @groups({"read"})
*/
private $center;
/**
* Get id
*
@@ -169,5 +194,41 @@ class PostalCode
{
return $this->country;
}
public function getRefPostalCodeId(): ?string
{
return $this->refPostalCodeId;
}
public function setRefPostalCodeId(?string $refPostalCodeId): self
{
$this->refPostalCodeId = $refPostalCodeId;
return $this;
}
public function getPostalCodeSource(): ?string
{
return $this->postalCodeSource;
}
public function setPostalCodeSource(?string $postalCodeSource): self
{
$this->postalCodeSource = $postalCodeSource;
return $this;
}
public function getCenter(): ?Point
{
return $this->center;
}
public function setCenter(?Point $center): self
{
$this->center = $center;
return $this;
}
}

View File

@@ -1,136 +0,0 @@
<?php
/*
* Copyright (C) 2015 Julien Fastré <julien.fastre@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\MainBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CenterType extends AbstractType
{
/**
* The user linked with this type.
*
* @var \Chill\MainBundle\Entity\User
*/
protected $user;
/**
* associative array where keys are center.id and
* value are center objects
*
* @var Center[]
*/
protected $reachableCenters = array();
/**
*
* @var CenterTransformer
*/
protected $transformer;
public function __construct(TokenStorageInterface $tokenStorage,
CenterTransformer $transformer)
{
$this->user = $tokenStorage->getToken()->getUser();
$this->transformer = $transformer;
$this->prepareReachableCenterByUser();
}
/**
* return a 'hidden' field if only one center is available.
*
* Return a 'choice' field if more than one center is available.
*
* @return string
* @throws \RuntimeException if the user is not associated with any center
*/
public function getParent()
{
$nbReachableCenters = count($this->reachableCenters);
if ($nbReachableCenters <= 1) {
return HiddenType::class;
} else {
return EntityType::class;
}
}
/**
* configure default options, i.e. add choices if user can reach multiple
* centers.
*
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
if (count($this->reachableCenters) > 1) {
$resolver->setDefault('class', Center::class)
->setDefault('choices', $this->reachableCenters)
->setDefault('placeholder', 'Pick a center')
;
}
}
/**
* add a data transformer if user can reach only one center
*
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($this->getParent() === HiddenType::class) {
$builder->addModelTransformer($this->transformer);
}
}
/**
* populate reachableCenters as an associative array where
* keys are center.id and value are center entities.
*
*/
private function prepareReachableCenterByUser()
{
$groupCenters = $this->user->getGroupCenters();
foreach ($groupCenters as $groupCenter) {
$center = $groupCenter->getCenter();
if (!array_key_exists($center->getId(),
$this->reachableCenters)) {
$this->reachableCenters[$center->getId()] = $center;
}
}
}
}

View File

@@ -24,14 +24,14 @@ use Symfony\Component\Form\FormInterface;
/**
* Available options :
*
*
* - `button_add_label`
* - `button_remove_label`
* - `identifier`: an identifier to identify the kind of collecton. Useful if some
* javascript should be launched associated to `add_entry`, `remove_entry` events.
*
* - `empty_collection_explain`
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class ChillCollectionType extends AbstractType
{
@@ -41,10 +41,11 @@ class ChillCollectionType extends AbstractType
->setDefaults([
'button_add_label' => 'Add an entry',
'button_remove_label' => 'Remove entry',
'identifier' => ''
'identifier' => '',
'empty_collection_explain' => '',
]);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['button_add_label'] = $options['button_add_label'];
@@ -52,8 +53,9 @@ class ChillCollectionType extends AbstractType
$view->vars['allow_delete'] = (int) $options['allow_delete'];
$view->vars['allow_add'] = (int) $options['allow_add'];
$view->vars['identifier'] = $options['identifier'];
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
}
public function getParent()
{
return \Symfony\Component\Form\Extension\Core\Type\CollectionType::class;

View File

@@ -0,0 +1,45 @@
<?php
namespace Chill\MainBundle\Form\Type\DataTransformer;
use Chill\MainBundle\Repository\AddressRepository;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
final class AddressToIdDataTransformer implements DataTransformerInterface
{
private AddressRepository $addressRepository;
public function __construct(AddressRepository $addressRepository)
{
$this->addressRepository = $addressRepository;
}
public function reverseTransform($value)
{
if (NULL === $value || '' === $value) {
return null;
}
$address = $this->addressRepository->find($value);
if (NULL === $address) {
$failure = new TransformationFailedException(sprintf("Address with id %s does not exists", $value));
$failure
->setInvalidMessage("The given {{ value }} is not a valid address id", [ '{{ value }}' => $value]);
throw $failure;
}
return $address;
}
public function transform($value)
{
if (NULL === $value) {
return '';
}
return $value->getId();
}
}

View File

@@ -20,36 +20,57 @@
namespace Chill\MainBundle\Form\Type\DataTransformer;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Repository\CenterRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
class CenterTransformer implements DataTransformerInterface
{
private EntityManagerInterface $em;
private CenterRepository $centerRepository;
private bool $multiple = false;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
public function __construct(
CenterRepository $centerRepository,
bool $multiple = false
) {
$this->centerRepository = $centerRepository;
$this->multiple = $multiple;
}
public function reverseTransform($id)
{
if ($id === NULL) {
return NULL;
if ($this->multiple) {
return new ArrayCollection();
} else {
return NULL;
}
}
$center = $this
->em
->getRepository(Center::class)
->find($id);
if ($this->multiple) {
$ids = \explode(',', $id);
} else {
$ids[] = (int) $id;
}
if ($center === NULL) {
$centers = $this
->centerRepository
->findBy(['id' => $ids ]);
if ([] === $centers || count($ids) > count($centers)) {
throw new TransformationFailedException(sprintf(
'No center found with id %d', $id));
'No center found for one of those ids: %s', implode(',', $ids)));
}
return $center;
if ($this->multiple) {
return new ArrayCollection($centers);
} else {
return $centers[0];
}
}
public function transform($center)
@@ -58,7 +79,21 @@ class CenterTransformer implements DataTransformerInterface
return '';
}
return $center->getId();
}
if ($this->multiple) {
if (!is_iterable($center)) {
throw new UnexpectedTypeException($center, \Traversable::class);
}
$ids = [];
foreach ($center as $c) {
$ids[] = $c->getId();
}
return implode(',', $ids);
} else {
if (!$center instanceof Center) {
throw new UnexpectedTypeException($center, Center::class);
}
return (string) $center->getId();
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Chill\MainBundle\Form\Type\Listing;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SearchType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\RequestStack;
final class FilterOrderType extends \Symfony\Component\Form\AbstractType
{
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** @var FilterOrderHelper $helper */
$helper = $options['helper'];
if ($helper->hasSearchBox()) {
$builder->add('q', SearchType::class, [
'label' => false,
'required' => false
]);
}
foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) {
switch($key) {
case 'q':
break;
case 'page':
$builder->add($key, HiddenType::class, [
'data' => 1
]);
break;
default:
$builder->add($key, HiddenType::class, [
'data' => $value
]);
break;
}
}
}
public function configureOptions(\Symfony\Component\OptionsResolver\OptionsResolver $resolver)
{
$resolver->setRequired('helper')
->setAllowedTypes('helper', FilterOrderHelper::class);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Form\Type\DataTransformer\AddressToIdDataTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Form type for picking an address.
*
* In the UI, this resolve to a vuejs component which will insert the created address id into the
* hidden's value. It will also allow to edit existing addresses without changing the id.
*
* In every page where this component is shown, you must include the required module:
*
* ```twig
* {% block js %}
* {{ encore_entry_script_tags('mod_input_address') }}
* {% endblock %}
*
* {% block css %}
* {{ encore_entry_link_tags('mod_input_address') }}
* {% endblock %}
* ```
*/
final class PickAddressType extends AbstractType
{
private AddressToIdDataTransformer $addressToIdDataTransformer;
private TranslatorInterface $translator;
public function __construct(
AddressToIdDataTransformer $addressToIdDataTransformer,
TranslatorInterface $translator
) {
$this->addressToIdDataTransformer = $addressToIdDataTransformer;
$this->translator = $translator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer($this->addressToIdDataTransformer);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['uniqid'] = $view->vars['attr']['data-input-address'] =\uniqid('input_address_');
$view->vars['attr']['data-use-valid-from'] = (int) $options['use_valid_from'];
$view->vars['attr']['data-use-valid-to'] = (int) $options['use_valid_to'];
$view->vars['attr']['data-button-text-create'] = $this->translator->trans($options['button_text_create']);
$view->vars['attr']['data-button-text-update'] = $this->translator->trans($options['button_text_update']);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => Address::class,
'use_valid_to' => false,
'use_valid_from' => false,
'button_text_create' => 'Create an address',
'button_text_update' => 'Update address',
// reset default from hidden type
'required' => true,
'error_bubbling' => false,
]);
}
public function getParent()
{
return HiddenType::class;
}
}

View File

@@ -0,0 +1,174 @@
<?php
/*
* Copyright (C) 2015 Julien Fastré <julien.fastre@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\MainBundle\Form\Type;
use Chill\MainBundle\Repository\CenterRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Security\Core\Security;
/**
* Pick a center
*
* For a given role and, eventually, scopes, show a dropdown (if more than
* one reachable center) or a HiddenType (if one or zero center).
*
*
*/
class PickCenterType extends AbstractType
{
protected AuthorizationHelperInterface $authorizationHelper;
protected Security $security;
protected CenterRepository $centerRepository;
public function __construct(
AuthorizationHelperInterface $authorizationHelper,
Security $security,
CenterRepository $centerRepository
) {
$this->authorizationHelper = $authorizationHelper;
$this->security = $security;
$this->centerRepository = $centerRepository;
}
/**
* return a 'hidden' field if only one center is available.
*
* Return a 'choice' field if more than one center is available.
*
* @return string
* @throws \RuntimeException if the user is not associated with any center
*/
/*
public function getParent()
{
$nbReachableCenters = count($this->reachableCenters);
if ($nbReachableCenters <= 1) {
return HiddenType::class;
} else {
return EntityType::class;
}
}
*/
/**
* configure default options, i.e. add choices if user can reach multiple
* centers.
*
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('class', Center::class)
->setRequired('role')
->setAllowedTypes('role', [ 'string' ])
->setDefault('scopes', [])
->setAllowedTypes('scopes', ['iterable'])
->setDefault('choice_options', [])
;
/*
->setDefault('choices', $this->reachableCenters)
->setDefault('placeholder', 'Pick a center')
;
*/
}
/**
* add a data transformer if user can reach only one center
*
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$centers = $this->getReachableCenters($options['role'], $options['scopes']);
if (count($centers) <= 1) {
$multiple = $options['choice_options']['multiple'] ?? false;
$builder->add('center', HiddenType::class);
$builder->get('center')->addModelTransformer(
new CenterTransformer($this->centerRepository, $multiple)
);
} else {
$builder->add('center', EntityType::class,
\array_merge(
$options['choice_options'],
[
'class' => Center::class,
'choices' => $centers
]
)
);
}
$builder
->addModelTransformer(new CallbackTransformer(
function($data) {
if (NULL === $data) {
return ['center' => null];
}
return ['center' => $data];
},
function($data) {
return $data['center'];
}
));
}
private function getReachableCenters(string $role, iterable $scopes): array
{
if (0 < count($scopes)) {
$centers = [];
foreach($scopes as $scope) {
foreach ($this->authorizationHelper
->getReachableCenters($this->security->getUser(), $role, $scope) as $center) {
$centers[spl_object_hash($center)] = $center;
}
}
return \array_values($centers);
} else {
return $this->authorizationHelper
->getReachableCenters($this->security->getUser(), $role);
}
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['is_hidden'] = count($this->getReachableCenters($options['role'],
$options['scopes'])) <= 1;
}
}

View File

@@ -20,23 +20,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\ThirdPartyBundle\Repository;
namespace Chill\MainBundle\Repository;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCivility;
use Chill\MainBundle\Entity\Civility;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method ThirdPartyCivility|null find($id, $lockMode = null, $lockVersion = null)
* @method ThirdPartyCivility|null findOneBy(array $criteria, array $orderBy = null)
* @method ThirdPartyCivility[] findAll()
* @method ThirdPartyCivility[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
* @method Civility|null find($id, $lockMode = null, $lockVersion = null)
* @method Civility|null findOneBy(array $criteria, array $orderBy = null)
* @method Civility[] findAll()
* @method Civility[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ThirdPartyCivilityRepository extends ServiceEntityRepository
class CivilityRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ThirdPartyCivility::class);
parent::__construct($registry, Civility::class);
}
}

View File

@@ -1,6 +1,9 @@
// Access to Bootstrap variables and mixins
@import '~ChillMainAssets/module/bootstrap/shared';
// Chill variables
@import './scss/chill_variables';
// Chill mixins
@import './scss/mixins';

View File

@@ -0,0 +1,13 @@
/*
* ADDITIONNAL COLORS VARIABLES
*/
// Contexts banners
$chill-person-context: $chill-green;
$chill-accourse-context: #718596;
$chill-household-context: #929d69;
// Badges colors
$social-issue-color: #4bafe8;
$social-action-color: $orange;
$activity-color: yellowgreen;

View File

@@ -8,45 +8,39 @@
div.flex-bloc,
div.flex-table {
display: flex;
align-items: stretch;
align-content: stretch;
box-sizing: border-box;
margin: 1.5em 0;
div.item-bloc {
display: flex;
@include border-collapse;
padding: 1em;
align-items: stretch;
align-content: stretch;
box-sizing: border-box;
margin: 1.5em 0;
div.item-row {
display: flex;
div.item-col:last-child {
div.item-bloc {
display: flex;
}
}
}
@include border-collapse;
padding: 1em;
h2,
h3,
h4,
dl,
p {
margin: 0;
}
h2,
h3,
h4 {
color: $blue;
}
div.item-row {
display: flex;
ul.record_actions {
margin: 0;
li {
margin-right: 5px;
div.item-col:last-child {
display: flex;
}
}
}
h2, h3, h4, dl, p {
margin: 0;
}
h2, h3, h4 {
color: $blue;
}
ul.record_actions {
margin: 0;
li {
margin-right: 5px;
}
}
}
}
/*
@@ -54,43 +48,36 @@ div.flex-table {
*/
div.flex-bloc {
flex-direction: row;
flex-wrap: wrap;
flex-direction: row;
flex-wrap: wrap;
div.item-bloc {
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;
flex-direction: column;
margin: 0;
hyphens: auto;
div.item-bloc {
flex-grow: 0; flex-shrink: 1; flex-basis: auto;
flex-direction: column;
margin: 0;
hyphens: auto;
div.item-row {
flex-grow: 1;
flex-shrink: 1;
flex-basis: auto;
flex-direction: column;
div.item-row {
flex-grow: 1; flex-shrink: 1; flex-basis: auto;
flex-direction: column;
div.item-col {
&:first-child {
flex-grow: 0;
flex-shrink: 0;
flex-basis: auto;
div.item-col {
&:first-child {
flex-grow: 0; flex-shrink: 0; flex-basis: auto;
}
&:last-child {
flex-grow: 1; flex-shrink: 1; flex-basis: auto;
@include separator;
ul.record_actions {
align-self: flex-end;
}
}
}
}
&:last-child {
flex-grow: 1;
flex-shrink: 1;
flex-basis: auto;
@include separator;
ul.record_actions {
align-self: flex-end;
}
}
}
}
}
}
/*
@@ -98,57 +85,53 @@ div.flex-bloc {
*/
div.flex-table {
flex-direction: column;
div.item-bloc {
flex-direction: column;
&:nth-child(even) {
background-color: $gray-200;
.chill-user-quote {
background-color: shade-color($gray-200, 5%);
}
}
div.item-row {
flex-direction: row;
div.item-col {
&:first-child {
flex-grow: 0;
flex-shrink: 0;
flex-basis: auto;
}
&:last-child {
flex-grow: 1;
flex-shrink: 1;
flex-basis: auto;
justify-content: flex-end;
@include media-breakpoint-down(md) {
@include separator;
}
ul.record_actions {
align-self: flex-start;
}
}
}
@include media-breakpoint-down(md) {
div.item-bloc {
flex-direction: column;
div.item-col {
&:last-child {
ul.record_actions {
align-self: flex-end;
&:nth-child(even) {
background-color: $gray-200;
.chill-user-quote {
background-color: shade-color($gray-200, 5%)
}
}
div.item-row {
flex-direction: row;
div.item-col {
&:first-child {
flex-grow: 0; flex-shrink: 0; flex-basis: auto;
}
&:last-child {
flex-grow: 1; flex-shrink: 1; flex-basis: auto;
justify-content: flex-end;
@include media-breakpoint-down(md) {
@include separator;
}
ul.record_actions {
align-self: flex-start; // !! conflict with .float-button.bottom see L297
}
}
}
@include media-breakpoint-down(md) {
flex-direction: column;
div.item-col {
&:last-child {
ul.record_actions {
align-self: flex-end;
}
}
}
}
}
}
}
}
}
}
/*
@@ -157,54 +140,54 @@ div.flex-table {
*/
div.wrap-list {
padding: 0;
width: 100%;
padding: 0;
width: 100%;
div.wl-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
div.wl-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
div.wl-col.title {
width: auto;
flex-shrink: 0;
div.wl-col.title {
width: auto;
flex-shrink: 0;
@include media-breakpoint-up(md) {
//margin-left: 1.5em;
}
@include media-breakpoint-up(md) {
//margin-left: 1.5em;
}
& > * {
padding-right: 1em;
}
& > * {
padding-right: 1em;
}
}
div.wl-col.list {
width: 75%;
margin: auto 0 0 auto;
.wl-item {
margin: 0.1em;
padding: 0em;
display: inline-block;
}
}
}
div.wl-col.list {
width: 75%;
margin: auto 0 0 auto;
&.debug .wl-row {
border: 1px solid $black;
.wl-item {
margin: 0.1em;
padding: 0em;
display: inline-block;
}
div.wl-col.title {
background-color: yellow;
}
div.wl-col.list {
background-color: cyan;
p.wl-item {
background-color: orange;
}
}
}
}
&.debug .wl-row {
border: 1px solid $black;
div.wl-col.title {
background-color: yellow;
}
div.wl-col.list {
background-color: cyan;
p.wl-item {
background-color: orange;
}
}
}
}
/*
@@ -213,56 +196,43 @@ div.wrap-list {
*/
div.wrap-header {
width: 100%;
width: 100%;
div.wh-row {
display: flex;
flex-direction: row;
&:first-child {
align-items: baseline;
}
&:last-child {
}
div.wh-col {
&:first-child {
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;
}
&:last-child {
flex-grow: 1;
flex-shrink: 1;
flex-basis: auto;
display: flex;
justify-content: flex-end;
}
}
}
&.debug {
border: 1px solid $black;
div.wh-row {
&:first-child div.wh-col {
display: flex;
flex-direction: row;
&:first-child {
background-color: $yellow;
align-items: baseline;
}
&:last-child {
background-color: $beige;
&:last-child {}
div.wh-col {
&:first-child {
flex-grow: 0; flex-shrink: 1; flex-basis: auto;
}
&:last-child {
flex-grow: 1; flex-shrink: 1; flex-basis: auto;
display: flex;
justify-content: flex-end;
}
}
}
&.debug {
border: 1px solid $black;
div.wh-row {
&:first-child div.wh-col {
&:first-child { background-color: $yellow; }
&:last-child { background-color: $beige; }
}
&:last-child div.wh-col {
&:first-child { background-color: $orange; }
&:last-child { background-color: $pink; }
}
}
}
&:last-child div.wh-col {
&:first-child {
background-color: $orange;
}
&:last-child {
background-color: $pink;
}
}
}
}
}
/*
@@ -273,67 +243,86 @@ div.flex-bloc,
div.flex-table,
div.wrap-list,
div.wrap-header {
div.separator {
@include separator;
}
ul.record_actions {
flex-grow: 1;
flex-shrink: 0;
flex-basis: auto;
margin: 0;
li {
margin-right: 5px;
div.separator {
@include separator;
}
ul.record_actions {
flex-grow: 1;
flex-shrink: 0;
flex-basis: auto;
margin: 0;
li {
margin-right: 5px;
}
}
}
}
/*
* FLOATBUTTON
* p-ê pas convaincant: cet asset est toujours en observation
*/
div.float-button {
width: 100%;
div.box {
width: 100%;
div.action {
float: right;
div.box {
width: 100%;
div.action {
float: right;
position: relative;
z-index: 1;
}
a {
word-break: break-all; // break email address
}
}
}
&.top {
div.action {
padding: 0 0 1em 1em;
&.top {
div.action {
padding: 0 0 1em 1em;
}
}
&.bottom {
display: flex;
overflow: hidden;
div.action {
height: calc(100% - 0em);
shape-outside: inset(calc(100% - 4em) 0 0);
display: flex;
align-items: flex-end;
padding: 0 0 0 1em;
* {
align-self: flex-end !important; // neutralize flex-table rule L118
}
ul.record_actions {
margin: 0;
padding: 0;
}
}
}
&.debug {
padding: 1em;
border: 1px solid black;
background-color: yellow;
div.box {
div.action {
background-color: transparentize(cyan, 0.4);
}
& > ul li,
& > div,
& > p {
background-color: transparentize(orange, 0.4);
}
& > div {
margin-bottom: 1em;
}
}
}
// avoid a position relative that make links unclickable
.fa-ul > li {
position: initial;
}
}
&.bottom {
display: flex;
overflow: hidden;
div.action {
height: calc(100% - 0em);
shape-outside: inset(calc(100% - 2em) 0 0);
display: flex;
align-items: flex-end;
padding: 0 0 0 1em;
* {
align-self: flex-end !important;
}
}
}
&.debug {
padding: 1em;
border: 1px solid black;
background-color: yellow;
div.action {
background-color: transparentize(#00ffff, 0.4);
}
}
}

View File

@@ -1,4 +1,3 @@
//
// Titles in forms
//
@@ -12,7 +11,6 @@
display: block;
}
// We use box-shadow instead of border
// to avoid to manage border double-width
// Then we can simulate border-collapse: collapse (table)

View File

@@ -1,27 +1,27 @@
/**
* Javascript file which handle ChillCollectionType
*
* Two events are emitted by this module, both on window and on collection / ul.
*
*
* Two events are emitted by this module, both on window and on collection / ul.
*
* Collection (an UL element) and entry (a li element) are associated with those
* events.
*
*
* ```
* window.addEventListener('collection-add-entry', function(e) {
* console.log(e.detail.collection);
* console.log(e.detail.entry);
* });
*
*
* window.addEventListener('collection-remove-entry', function(e) {
* console.log(e.detail.collection);
* console.log(e.detail.entry);
* });
*
*
* collection.addEventListener('collection-add-entry', function(e) {
* console.log(e.detail.collection);
* console.log(e.detail.entry);
* });
*
*
* collection.addEventListener('collection-remove-entry', function(e) {
* console.log(e.detail.collection);
* console.log(e.detail.entry);
@@ -38,7 +38,7 @@ class CollectionEvent {
}
/**
*
*
* @param {type} button
* @returns {handleAdd}
*/
@@ -47,6 +47,7 @@ var handleAdd = function(button) {
form_name = button.dataset.collectionAddTarget,
prototype = button.dataset.formPrototype,
collection = document.querySelector('ul[data-collection-name="'+form_name+'"]'),
empty_explain = collection.querySelector('li[data-collection-empty-explain]'),
entry = document.createElement('li'),
event = new CustomEvent('collection-add-entry', { detail: { collection: collection, entry: entry } }),
counter = collection.childNodes.length,
@@ -56,8 +57,11 @@ var handleAdd = function(button) {
entry.innerHTML = content;
entry.classList.add('entry');
initializeRemove(collection, entry);
if (empty_explain !== null) {
empty_explain.remove();
}
collection.appendChild(entry);
collection.dispatchEvent(event);
window.dispatchEvent(event);
};
@@ -70,30 +74,30 @@ var initializeRemove = function(collection, entry) {
allowDelete = collection.dataset.collectionAllowDelete,
event = new CustomEvent('collection-remove-entry', { detail: { collection: collection, entry: entry } })
;
if (allowDelete === '0' && isPersisted === '1') {
return;
}
button.classList.add('btn', 'btn-delete', 'remove-entry');
button.textContent = content;
button.addEventListener('click', function(e) {
e.preventDefault();
entry.remove();
collection.dispatchEvent(event);
window.dispatchEvent(event);
});
entry.appendChild(button);
};
window.addEventListener('load', function() {
var
var
addButtons = document.querySelectorAll("button[data-collection-add-target]"),
collections = document.querySelectorAll("ul[data-collection-name]")
;
for (let i = 0; i < addButtons.length; i ++) {
let addButton = addButtons[i];
addButton.addEventListener('click', function(e) {
@@ -101,11 +105,15 @@ window.addEventListener('load', function() {
handleAdd(e.target);
});
}
for (let i = 0; i < collections.length; i ++) {
let entries = collections[i].querySelectorAll(':scope > li');
for (let j = 0; j < entries.length; j ++) {
console.log(entries[j].dataset);
if (entries[j].dataset.collectionEmptyExplain === "1") {
continue;
}
initializeRemove(collections[i], entries[j]);
}
}

View File

@@ -0,0 +1,16 @@
/**
* Submit button(s) are disabled as soon as submit button is clicked and form is submitted.
* ID 'create-form' must be added to submit forms.
*/
var form = document.getElementById('create-form');
var submitButtons = document.querySelectorAll("[type=submit]");
form.addEventListener('submit', function(e){
for(var i=0; i<submitButtons.length; i++){
submitButtons[i].disabled = true;
}
})

View File

@@ -17,7 +17,8 @@ export default {
components: {
AddAddress
},
props: ['addAddress'],
props: ['addAddress', 'callback'],
emits: ['addressEdited', 'addressCreated'],
computed: {
context() {
return this.addAddress.context;
@@ -46,6 +47,7 @@ export default {
// address is already linked, just finish !
this.$refs.addAddress.afterLastPaneAction({});
this.$emit('addressEdited', payload);
// New created address
} else {
@@ -57,6 +59,8 @@ export default {
* Post new created address to targetEntity
*/
postAddressTo(payload) {
this.$emit('addressCreated', payload);
console.log('postAddress', payload.addressId, 'To', payload.target, payload.targetId);
switch (payload.target) {
case 'household':

View File

@@ -267,6 +267,9 @@ export default {
title: { create: 'add_an_address_title', edit: 'edit_address' },
openPanesInModal: true,
stickyActions: false,
// show a message when no address.
// if set to undefined, the value will be equivalent to false if stickyActions is false, true otherwise.
showMessageWhenNoAddress: undefined,
useDate: {
validFrom: false,
validTo: false

View File

@@ -15,7 +15,7 @@
<span v-if="forceRedirect">{{ $t('wait_redirection') }}</span>
</div>
<div v-if="noAddressWithStickyActions" class="mt-5">
<div v-if="showMessageWhenNoAddress" class="mt-5">
<p class="chill-no-data-statement">
{{ $t('not_yet_address') }}
</p>
@@ -50,8 +50,8 @@ export default {
},
props: [
'context',
'options',
'defaultz',
'options',
'flag',
'entity',
'errorMsg',
@@ -91,7 +91,11 @@ export default {
forceRedirect() {
return (!(this.context.backUrl === null || typeof this.context.backUrl === 'undefined'));
},
noAddressWithStickyActions() {
showMessageWhenNoAddress() {
let showMessageWhenNoAddress = this.options.showMessageWhenNoAddress === undefined ? this.defaultz.showMessageWhenNoAddress : this.options.showMessageWhenNoAddress;
if (showMessageWhenNoAddress === true || showMessageWhenNoAddress === false) {
return !this.context.edit && !this.address.id && showMessageWhenNoAddress;
}
return !this.context.edit && !this.address.id && this.options.stickyActions;
}
}

View File

@@ -0,0 +1,86 @@
import {createApp} from 'vue';
import {_createI18n} from 'ChillMainAssets/vuejs/_js/i18n';
import {addressMessages} from './i18n';
import App from './App.vue';
const i18n = _createI18n(addressMessages);
let inputs = document.querySelectorAll('input[type="hidden"][data-input-address]');
const isNumeric = function(v) { return !isNaN(v); };
inputs.forEach(el => {
let
addressId = el.value,
uniqid = el.dataset.inputAddress,
container = document.querySelector('div[data-input-address-container="' + uniqid + '"]'),
isEdit = addressId !== '',
addressIdInt = addressId !== '' ? parseInt(addressId) : null
;
if (container === null) {
throw Error("no container");
}
console.log('useValidFrom', el.dataset.useValidFrom === '1');
const app = createApp({
template: `<app v-bind:addAddress="this.addAddress" @address-created="associateToInput"></app>`,
data() {
return {
addAddress: {
context: {
// for legacy ? can be remove ?
target: {
name: 'input-address',
id: addressIdInt,
},
edit: isEdit,
addressId: addressIdInt,
},
options: {
/// Options override default.
/// null value take default component value defined in AddAddress data()
button: {
text: {
create: el.dataset.buttonTextCreate || null,
edit: el.dataset.buttonTextUpdate || null,
},
size: null,
displayText: true
},
/// Modal title text if create or edit address (trans chain, see i18n)
title: {
create: null,
edit: null,
},
/// Display panes in Modal for step123
openPanesInModal: true,
/// Display actions buttons of panes in a sticky-form-button navbar
stickyActions: false,
showMessageWhenNoAddress: true,
/// Use Date fields
useDate: {
validFrom: el.dataset.useValidFrom === '1' || false, //boolean, default: false
validTo: el.dataset.useValidTo === '1' || false, //boolean, default: false
},
/// Don't display show renderbox Address: showPane display only a button
onlyButton: false,
}
}
}
},
methods: {
associateToInput(payload) {
el.value = payload.addressId;
}
}
})
.use(i18n)
.component('app', App)
.mount(container);
});

View File

@@ -74,7 +74,12 @@ export default {
case 'thirdparty':
let data = this.$refs.castThirdparty.$data.thirdparty;
data.name = data.text;
data.address = { id: data.address.address_id }
if (data.address !== undefined) {
data.address = { id: data.address.address_id }
} else {
data.address = null;
}
return data;
default:
throw Error('Invalid type of entity')

View File

@@ -1,5 +1,5 @@
{% set formId = crudMainFormId|default('crud_main_form') %}
<div class="{% block crud_content_main_div_class %}{% endblock %}">
<div class="{% block crud_content_main_div_class %}col-10 centered{% endblock %}">
{% block crud_content_header %}
<h1>{{ ('crud.'~crud_name~'.title_edit')|trans }}</h1>
{% endblock crud_content_header %}

View File

@@ -1,9 +1,15 @@
<div class="col-10 centered">
{% block index_header %}
<h1>{{ ('crud.' ~ crud_name ~ '.index.title')|trans({'%crud_name%': crud_name}) }}</h1>
{% endblock index_header %}
{% block filter_order %}
{% if filter_order is not null %}
{{ filter_order|chill_render_filter_order_helper }}
{% endif %}
{% endblock %}
{% if entities|length == 0 %}
{% block no_existing_entities %}
<p>{{ no_existing_entities_sentences|default('No entities')|trans }}</p>
@@ -32,17 +38,20 @@
{% endif %}
<div class="crud_index__pagination">
{{ chill_pagination(paginator) }}
</div>
{% block pagination %}
<div class="crud_index__pagination">
{{ chill_pagination(paginator) }}
</div>
{% endblock %}
{% block list_actions %}
<ul class="record_actions sticky-form-buttons">
{% block add_new %}
<li>
<a href="{{ chill_path_add_return_path('chill_crud_' ~ crud_name ~ '_new') }}" class="btn btn-new">{{ ('crud.'~crud_name~'.index.add_new')|trans( {'%crud_name%': crud_name} ) }}</a>
</li>
{% endblock %}
{% block actions_before %}{% endblock %}
{% block add_new %}
<li>
<a href="{{ chill_path_add_return_path('chill_crud_' ~ crud_name ~ '_new') }}" class="btn btn-new">{{ ('crud.'~crud_name~'.index.add_new')|trans( {'%crud_name%': crud_name} ) }}</a>
</li>
{% endblock %}
</ul>
{% endblock list_actions %}
</div>

View File

@@ -1,10 +1,11 @@
{% set formId = crudMainFormId|default('crud_main_form') %}
<div class="{% block crud_content_main_div_class %}col-10 centered{% endblock %}">
{% block crud_content_header %}
<h1>{{ ('crud.' ~ crud_name ~ '.title_new')|trans({'%crud_name%' : crud_name }) }}</h1>
{% endblock crud_content_header %}
{% block crud_content_form %}
{{ form_start(form) }}
{{ form_start(form, { 'attr' : { 'id': formId } }) }}
{% block crud_content_form_rows %}
{% for f in form %}{% if f.vars.name != 'submit' %}
@@ -14,6 +15,8 @@
{% block crud_content_after_form %}{% endblock %}
{{ form_end(form) }}
{% block crud_content_form_actions %}
<ul class="record_actions sticky-form-buttons">
{% block content_form_actions_back %}
@@ -25,21 +28,21 @@
{% endblock %}
{% block content_form_actions_save_and_close %}
<li class="">
<button type="submit" name="submit" value="save-and-close" class="btn btn-create">
<button type="submit" name="submit" value="save-and-close" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_close'|trans }}
</button>
</li>
{% endblock %}
{% block content_form_actions_save_and_show %}
<li class="">
<button type="submit" name="submit" value="save-and-show" class="btn btn-create">
<button type="submit" name="submit" value="save-and-show" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_show'|trans }}
</button>
</li>
{% endblock %}
{% block content_form_actions_save_and_new %}
<li class="">
<button type="submit" name="submit" value="save-and-new" class="btn btn-create">
<button type="submit" name="submit" value="save-and-new" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_new'|trans }}
</button>
</li>

View File

@@ -0,0 +1,12 @@
{{ form_start(form) }}
<div class="chill_filter_order container">
<div class="row">
<div class="col-md-12">
<div class="input-group mb-3">
{{ form_widget(form.q)}}
<button type="submit" class="btn btn-chill-l-gray"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
</div>
{{ form_end(form) }}

View File

@@ -168,6 +168,10 @@
{{ form_widget(entry) }}
</div>
</li>
{% else %}
<li data-collection-empty-explain="1">
<span class="chill-no-data-statement">{{ form.vars.empty_collection_explain|default('No item')|trans }}</span>
</li>
{% endfor %}
</ul>
@@ -198,3 +202,15 @@
{{ form_widget(entry) }}
{% endfor %}
{% endblock comment_widget %}
{% block pick_center_widget %}
{{ form_widget(form.center) }}
{% endblock pick_center_widget %}
{% block pick_center_row %}
{% if (not form.vars.is_hidden) %}
{{ block('form_row') }}
{% else %}
{{ form_widget(form.center) }}
{% endif %}
{% endblock %}

View File

@@ -27,7 +27,7 @@ class SearchUserApiProvider implements SearchApiInterface
->setSelectPertinence("GREATEST(SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical),
SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical))", [ $pattern, $pattern ])
->setFromClause("users AS u")
->setWhereClause("SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15
->setWhereClauses("SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15
OR
SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical) > 0.15
", [ $pattern, $pattern ]);

View File

@@ -12,8 +12,8 @@ class SearchApiQuery
private array $pertinenceParams = [];
private ?string $fromClause = null;
private array $fromClauseParams = [];
private ?string $whereClause = null;
private array $whereClauseParams = [];
private array $whereClauses = [];
private array $whereClausesParams = [];
public function setSelectKey(string $selectKey, array $params = []): self
{
@@ -47,16 +47,39 @@ class SearchApiQuery
return $this;
}
public function setWhereClause(string $whereClause, array $params = []): self
/**
* Set the where clause and replace all existing ones.
*
*/
public function setWhereClauses(string $whereClause, array $params = []): self
{
$this->whereClause = $whereClause;
$this->whereClauseParams = $params;
$this->whereClauses = [$whereClause];
$this->whereClausesParams = [$params];
return $this;
}
/**
* Add a where clause.
*
* This will add to previous where clauses with and `AND` join
*
* @param string $whereClause
* @param array $params
* @return $this
*/
public function andWhereClause(string $whereClause, array $params = []): self
{
$this->whereClauses[] = $whereClause;
$this->whereClausesParams[] = $params;
return $this;
}
public function buildQuery(): string
{
$where = \implode(' AND ', $this->whereClauses);
return \strtr("SELECT
'{key}' AS key,
{metadata} AS metadata,
@@ -68,7 +91,7 @@ class SearchApiQuery
'{metadata}' => $this->jsonbMetadata,
'{pertinence}' => $this->pertinence,
'{from}' => $this->fromClause,
'{where}' => $this->whereClause,
'{where}' => $where,
]);
}
@@ -79,7 +102,7 @@ class SearchApiQuery
$this->jsonbMetadataParams,
$this->pertinenceParams,
$this->fromClauseParams,
$this->whereClauseParams,
\array_merge([], ...$this->whereClausesParams),
);
}
}

View File

@@ -41,9 +41,8 @@ use Chill\MainBundle\Entity\RoleScope;
*
* Provides methods for user and entities information.
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class AuthorizationHelper
class AuthorizationHelper implements AuthorizationHelperInterface
{
protected RoleHierarchyInterface $roleHierarchy;
@@ -203,9 +202,9 @@ class AuthorizationHelper
* @param User $user
* @param string|Role $role
* @param null|Scope $scope
* @return Center[]
* @return Center[]|array
*/
public function getReachableCenters(User $user, $role, Scope $scope = null)
public function getReachableCenters(User $user, string $role, ?Scope $scope = null): array
{
if ($role instanceof Role) {
$role = $role->getRole();
@@ -267,9 +266,9 @@ class AuthorizationHelper
* @param User $user
* @param string role
* @param Center|Center[] $center
* @return Scope[]
* @return Scope[]|array
*/
public function getReachableScopes(User $user, $role, $center)
public function getReachableScopes(User $user, string $role, $center): array
{
if ($role instanceof Role) {
$role = $role->getRole();

View File

@@ -0,0 +1,32 @@
<?php
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Security\Core\Role\Role;
interface AuthorizationHelperInterface
{
/**
* Get reachable Centers for the given user, role,
* and optionnaly Scope
*
* @param User $user
* @param string|Role $role
* @param null|Scope $scope
* @return Center[]
*/
public function getReachableCenters(User $user, string $role, ?Scope $scope = null): array;
/**
* @param User $user
* @param string $role
* @param Center|Center[]|array $center
* @return array
*/
public function getReachableScopes(User $user, string $role, $center): array;
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Chill\MainBundle\Templating\Listing;
use Chill\MainBundle\Form\Type\Listing\FilterOrderType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class FilterOrderHelper
{
private FormFactoryInterface $formFactory;
private RequestStack $requestStack;
private ?array $searchBoxFields = null;
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
}
public function setSearchBox($searchBoxFields = null): self
{
$this->searchBoxFields = $searchBoxFields;
return $this;
}
public function hasSearchBox(): bool
{
return $this->searchBoxFields !== null;
}
private function getFormData(): array
{
return [
'q' => $this->getQueryString()
];
}
public function getQueryString(): ?string
{
$q = $this->requestStack->getCurrentRequest()
->query->get('q', null);
return empty($q) ? NULL : $q;
}
public function buildForm($name = null, string $type = FilterOrderType::class, array $options = []): FormInterface
{
return $this->formFactory
->createNamed($name, $type, $this->getFormData(), \array_merge([
'helper' => $this,
'method' => 'GET',
'csrf_protection' => false,
], $options));
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Chill\MainBundle\Templating\Listing;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class FilterOrderHelperBuilder
{
private ?array $searchBoxFields = null;
private FormFactoryInterface $formFactory;
private RequestStack $requestStack;
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
}
public function addSearchBox(array $fields, ?array $options = []): self
{
$this->searchBoxFields = $fields;
return $this;
}
public function build(): FilterOrderHelper
{
$helper = new FilterOrderHelper(
$this->formFactory,
$this->requestStack
);
$helper->setSearchBox($this->searchBoxFields);
return $helper;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Chill\MainBundle\Templating\Listing;
use Symfony\Component\Form\FormFactoryBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface
{
private FormFactoryInterface $formFactory;
private RequestStack $requestStack;
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
}
public function create(string $context, ?array $options = []): FilterOrderHelperBuilder
{
return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Chill\MainBundle\Templating\Listing;
interface FilterOrderHelperFactoryInterface
{
public function create(string $context, ?array $options = []): FilterOrderHelperBuilder;
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Chill\MainBundle\Templating\Listing;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class Templating extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('chill_render_filter_order_helper', [$this, 'renderFilterOrderHelper'], [
'needs_environment' => true, 'is_safe' => ['html'],
])
];
}
public function renderFilterOrderHelper(
Environment $environment,
FilterOrderHelper $helper,
?string $template = '@ChillMain/FilterOrder/base.html.twig',
?array $options = []
) {
return $environment->render($template, [
'helper' => $helper,
'form' => $helper->buildForm()->createView(),
'options' => $options
]);
}
}

View File

@@ -28,18 +28,19 @@ use Symfony\Bridge\Doctrine\Form\Type\EntityType;
/**
*
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CenterTypeTest extends TypeTestCase
{
/**
* Test that a user which can reach only one center
* Test that a user which can reach only one center
* render as an hidden field
*/
public function testUserCanReachSingleCenter()
{
$this->markTestSkipped();
//prepare user
$center = $this->prepareCenter(1, 'center');
$groupCenter = (new GroupCenter())
@@ -47,18 +48,19 @@ class CenterTypeTest extends TypeTestCase
;
$user = (new User())
->addGroupCenter($groupCenter);
$type = $this->prepareType($user);
$this->assertEquals(HiddenType::class, $type->getParent());
}
/**
* Test that a user which can reach only one center
* Test that a user which can reach only one center
* render as an hidden field
*/
public function testUserCanReachMultipleSameCenter()
{
$this->markTestSkipped();
//prepare user
$center = $this->prepareCenter(1, 'center');
$groupCenterA = (new GroupCenter())
@@ -70,18 +72,19 @@ class CenterTypeTest extends TypeTestCase
$user = (new User())
->addGroupCenter($groupCenterA)
->addGroupCenter($groupCenterB);
$type = $this->prepareType($user);
$this->assertEquals(HiddenType::class, $type->getParent());
}
/**
* Test that a user which can reach multiple center
* Test that a user which can reach multiple center
* make CenterType render as "entity" type.
*/
public function testUserCanReachMultipleCenters()
{
$this->markTestSkipped();
//prepare user
$centerA = $this->prepareCenter(1, 'centerA');
$centerB = $this->prepareCenter(2, 'centerB');
@@ -95,61 +98,61 @@ class CenterTypeTest extends TypeTestCase
->addGroupCenter($groupCenterA)
->addGroupCenter($groupCenterB)
;
$type = $this->prepareType($user);
$this->assertEquals(EntityType::class, $type->getParent());
}
/**
* prepare a mocked center, with and id and name given
*
*
* @param int $id
* @param string $name
* @return \Chill\MainBundle\Entity\Center
* @return \Chill\MainBundle\Entity\Center
*/
private function prepareCenter($id, $name)
{
$prophet = new \Prophecy\Prophet;
$prophecyCenter = $prophet->prophesize();
$prophecyCenter->willExtend('\Chill\MainBundle\Entity\Center');
$prophecyCenter->getId()->willReturn($id);
$prophecyCenter->getName()->willReturn($name);
return $prophecyCenter->reveal();
}
/**
* prepare the type with mocked center transformer and token storage
*
*
* @param User $user the user for wich the form will be prepared
* @return CenterType
*/
private function prepareType(User $user)
{
$prophet = new \Prophecy\Prophet;
$prophet = new \Prophecy\Prophet;
//create a center transformer
$centerTransformerProphecy = $prophet->prophesize();
$centerTransformerProphecy
->willExtend('Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer');
$transformer = $centerTransformerProphecy->reveal();
$tokenProphecy = $prophet->prophesize();
$tokenProphecy
->willImplement('\Symfony\Component\Security\Core\Authentication\Token\TokenInterface');
$tokenProphecy->getUser()->willReturn($user);
$token = $tokenProphecy->reveal();
$tokenStorageProphecy = $prophet->prophesize();
$tokenStorageProphecy
->willExtend('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage');
$tokenStorageProphecy->getToken()->willReturn($token);
$tokenStorage = $tokenStorageProphecy->reveal();
return new CenterType($tokenStorage, $transformer);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Search;
use Chill\MainBundle\Search\SearchApiQuery;
use PHPUnit\Framework\TestCase;
class SearchApiQueryTest extends TestCase
{
public function testMultipleWhereClauses()
{
$q = new SearchApiQuery();
$q->setSelectJsonbMetadata('boum')
->setSelectKey('bim')
->setSelectPertinence('1')
->setFromClause('badaboum')
->andWhereClause('foo', [ 'alpha' ])
->andWhereClause('bar', [ 'beta' ])
;
$query = $q->buildQuery();
$this->assertStringContainsString('foo AND bar', $query);
$this->assertEquals(['alpha', 'beta'], $q->buildParameters());
}
public function testWithoutWhereClause()
{
$q = new SearchApiQuery();
$q->setSelectJsonbMetadata('boum')
->setSelectKey('bim')
->setSelectPertinence('1')
->setFromClause('badaboum')
;
$this->assertTrue(\is_string($q->buildQuery()));
$this->assertEquals([], $q->buildParameters());
}
}

View File

@@ -58,7 +58,9 @@ module.exports = function(encore, entries)
encore.addEntry('mod_forkawesome', __dirname + '/Resources/public/module/forkawesome/index.js');
encore.addEntry('mod_bootstrap', __dirname + '/Resources/public/module/bootstrap/index.js');
encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index.js');
encore.addEntry('mod_disablebuttons', __dirname + '/Resources/public/module/disable-buttons/index.js');
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
// Vue entrypoints
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');
encore.addEntry('vue_onthefly', __dirname + '/Resources/public/vuejs/OnTheFly/index.js');

View File

@@ -60,11 +60,6 @@ services:
tags:
- { name: twig.extension }
chill.main.form.data_transformer.center_transformer:
class: Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer
arguments:
- "@doctrine.orm.entity_manager"
chill.main.validator.role_scope_scope_presence:
class: Chill\MainBundle\Validation\Validator\RoleScopeScopePresence
arguments:

View File

@@ -1,4 +1,5 @@
services:
chill.main.form.type.translatable.string:
class: Chill\MainBundle\Form\Type\TranslatableStringFormType
arguments:
@@ -36,13 +37,9 @@ services:
tags:
- { name: form.type, alias: select2_chill_language }
chill.main.form.type.center:
class: Chill\MainBundle\Form\Type\CenterType
arguments:
- "@security.token_storage"
- "@chill.main.form.data_transformer.center_transformer"
tags:
- { name: form.type, alias: center }
Chill\MainBundle\Form\Type\PickCenterType:
autowire: true
autoconfigure: true
chill.main.form.type.composed_role_scope:
class: Chill\MainBundle\Form\Type\ComposedRoleScopeType
@@ -97,6 +94,10 @@ services:
arguments:
- '@Chill\MainBundle\Export\ExportManager'
Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer:
autowire: true
autoconfigure: true
chill.main.form.advanced_search_type:
class: Chill\MainBundle\Form\AdvancedSearchType
autowire: true
@@ -128,3 +129,11 @@ services:
tags:
- { name: form.type }
Chill\MainBundle\Form\Type\PickAddressType:
autoconfigure: true
autowire: true
Chill\MainBundle\Form\DataTransform\AddressToIdDataTransformer:
autoconfigure: true
autowire: true

View File

@@ -38,6 +38,7 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Security\Authorization\AuthorizationHelper: '@chill.main.security.authorization.helper'
Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface: '@chill.main.security.authorization.helper'
chill.main.role_provider:
class: Chill\MainBundle\Security\RoleProvider

View File

@@ -36,7 +36,7 @@ services:
autowire: true
tags:
- { name: 'chill.render_entity' }
Chill\MainBundle\Templating\ChillMarkdownRenderExtension:
tags:
- { name: twig.extension }
@@ -46,4 +46,10 @@ services:
- '@Symfony\Component\Templating\EngineInterface'
tags:
- { name: 'chill.render_entity' }
Chill\MainBundle\Templating\Listing\:
resource: './../../Templating/Listing'
autoconfigure: true
autowire: true
Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface: '@Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory'

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add some fields to PostalCode
*/
final class Version20211006151653 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add some fields to PostalCode';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_postal_code ADD refPostalCodeId VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE chill_main_postal_code ADD postalCodeSource VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE chill_main_postal_code ADD center geometry(POINT,4326) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_postal_code DROP refPostalCodeId');
$this->addSql('ALTER TABLE chill_main_postal_code DROP postalCodeSource');
$this->addSql('ALTER TABLE chill_main_postal_code DROP center');
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Create civility table
*/
final class Version20211007150019 extends AbstractMigration
{
public function getDescription(): string
{
return 'create civility table';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_main_civility_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_main_civility (id INT NOT NULL, name JSON NOT NULL, abbreviation JSON NOT NULL, active BOOLEAN NOT NULL, PRIMARY KEY(id))');
}
public function down(Schema $schema): void
{
$this->addSql('DROP SEQUENCE chill_main_civility_id_seq');
$this->addSql('DROP TABLE chill_main_civility');
}
}

View File

@@ -56,6 +56,7 @@ centers: centres
Centers: Centres
comment: commentaire
Comment: Commentaire
Any comment: Aucun commentaire
# comment embeddable
No comment associated: Aucun commentaire
@@ -83,6 +84,8 @@ address more:
extra: ""
distribution: cedex
Create a new address: Créer une nouvelle adresse
Create an address: Créer une adresse
Update address: Modifier l'adresse
#serach
Your search is empty. Please provide search terms.: La recherche est vide. Merci de fournir des termes de recherche.

View File

@@ -155,7 +155,7 @@ final class PersonController extends AbstractController
$person = $this->_getPerson($person_id);
if ($person === null) {
return $this->createNotFoundException();
throw $this->createNotFoundException();
}
$this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person,

View File

@@ -84,6 +84,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$loader->load('services/repository.yaml');
$loader->load('services/serializer.yaml');
$loader->load('services/security.yaml');
$loader->load('services/doctrineEventListener.yaml');
// load service advanced search only if configure
if ($config['search']['search_by_phone'] != 'never') {

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Chill\PersonBundle\EventListener;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use LogicException;
use Symfony\Component\Validator\Exception\LogicException as ExceptionLogicException;
class PersonEventListener
{
public function prePersistPerson(Person $person): void
{
$firstnameCaps = mb_convert_case(mb_strtolower($person->getFirstName()), MB_CASE_TITLE, 'UTF-8');
$firstnameCaps = ucwords(strtolower($firstnameCaps), " \t\r\n\f\v'-");
$person->setFirstName($firstnameCaps);
$lastnameCaps = mb_strtoupper($person->getLastName(), 'UTF-8');
$person->setLastName($lastnameCaps);
}
public function prePersistAltName(PersonAltName $altname)
{
$altnameCaps = mb_strtoupper($altname->getLabel(), 'UTF-8');
$altname->setLabel($altnameCaps);
}
}

View File

@@ -22,7 +22,9 @@
namespace Chill\PersonBundle\Form;
use Chill\MainBundle\Form\Event\CustomizeFormEvent;
use Chill\MainBundle\Repository\CenterRepository;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -30,12 +32,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\CenterType;
use Chill\MainBundle\Form\Type\PickCenterType;
use Chill\PersonBundle\Form\Type\GenderType;
use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\MainBundle\Form\Type\Export\PickCenterType;
final class CreationPersonType extends AbstractType
{
@@ -43,11 +44,7 @@ final class CreationPersonType extends AbstractType
// TODO: See if this is still valid and update accordingly.
const NAME = 'chill_personbundle_person_creation';
/**
*
* @var CenterTransformer
*/
private $centerTransformer;
private CenterRepository $centerRepository;
/**
*
@@ -58,11 +55,11 @@ final class CreationPersonType extends AbstractType
private EventDispatcherInterface $dispatcher;
public function __construct(
CenterTransformer $centerTransformer,
CenterRepository $centerRepository,
ConfigPersonAltNamesHelper $configPersonAltNamesHelper,
EventDispatcherInterface $dispatcher
) {
$this->centerTransformer = $centerTransformer;
$this->centerTransformer = $centerRepository;
$this->configPersonAltNamesHelper = $configPersonAltNamesHelper;
$this->dispatcher = $dispatcher;
}
@@ -82,8 +79,9 @@ final class CreationPersonType extends AbstractType
->add('gender', GenderType::class, array(
'required' => true, 'placeholder' => null
))
->add('center', CenterType::class, [
'required' => false
->add('center', PickCenterType::class, [
'required' => false,
'role' => PersonVoter::CREATE,
])
;

View File

@@ -1,10 +1,13 @@
// Access to Bootstrap variables and mixins
@import '~ChillMainAssets/module/bootstrap/shared';
@import 'ChillMainAssets/chill/scss/chill_variables';
// Complete/override Main generic assets
@import './scss/mixins';
@import './scss/render_box.scss';
@import './scss/flex_table.scss';
@import './scss/badge.scss';
// Specific templates styles
@import './scss/accompanying_period_work.scss';
@@ -19,13 +22,13 @@
div.banner {
div#header-person-name {
background: none repeat scroll 0 0 $chill-green-dark;
background: none repeat scroll 0 0 shade-color($chill-person-context, 20%);
color: $white;
padding-top: 1em;
padding-bottom: 1em;
}
div#header-person-details {
background: none repeat scroll 0 0 $chill-green;
background: none repeat scroll 0 0 $chill-person-context;
color: $white;
padding-top: 1em;
padding-bottom: 1em;
@@ -92,7 +95,6 @@ div.person-view {
* Header custom for Accompanying Course
*/
$chill-accourse-context: #718596;
div.banner {
div#header-accompanying_course-name {
@@ -104,9 +106,9 @@ div.banner {
span {
a {
color: $white;
}
a:hover {
text-decoration: underline;
&:hover {
text-decoration: underline;
}
}
}
}
@@ -118,19 +120,11 @@ div.banner {
}
}
abbr.referrer {
font-size: 70%;
padding-right: 0.4em;
align-self: center; // in flex context
}
/*
* HOUSEHOLD CONTEXT
* Header custom for Household
*/
$chill-household-context: #929d69;
div.banner {
div#header-household-name {
background: none repeat scroll 0 0 $chill-household-context;
@@ -142,9 +136,9 @@ div.banner {
span {
a {
color: $white;
}
a:hover {
text-decoration: underline;
&:hover {
text-decoration: underline;
}
}
}
div.household-members {
@@ -185,6 +179,7 @@ div.banner {
}
}
///
div.household-resume {
display: flex;
flex-direction: row;
@@ -208,48 +203,10 @@ div.household-resume {
}
}
/*
* BADGES, MARKS, PINS
* for chill person theme
*/
// chill person badges
span.badge-person,
span.badge-thirdparty {
display: inline-block;
padding: 0 0.5em !important;
background-color: $white;
color: $dark;
border: 1px solid $chill-ll-gray;
border-bottom-width: 2px;
border-bottom-style: solid;
border-radius: 6px;
a {
text-decoration: none;
}
}
span.badge-person {
border-bottom-color: $chill-green;
}
// todo: move in thirdparty
span.badge-thirdparty {
border-bottom-color: shade-color($chill-pink, 10%);
}
// household holder mark
span.fa-holder {
width: 1em;
margin: -10px 0.3em -8px 0;
i:last-child {
font-weight: 900;
color: white;
font-size: 70%;
font-family: "Open Sans Extrabold";
}
}
/// Horizontal list of persons (Accourse resume page)
div.accompanyingcourse-resume {
div.associated-persons {
font-size: 110%;
span.household {
display: inline-block;
border-radius: 8px;
@@ -272,3 +229,16 @@ div.accompanyingcourse-resume {
}
}
}
///
abbr.referrer { // still used ?
font-size: 70%;
padding-right: 0.4em;
align-self: center; // in flex context
}
.updatedBy {
margin-top: 0.3rem;
font-size: 0.9rem;
font-style: italic;
}

View File

@@ -1,31 +1,6 @@
/// AccompanyingCourse Work list Page
div.accompanying_course_work-list {
h2.title {
display: flex;
flex-direction: row;
width: 100%;
color: $dark;
//height: 40px;
span.title_label {
border-radius: 0.35rem 0 0 0.35rem;
background-color: $social-action-label-color;
color: $white;
font-size: 80%;
padding: 0.5em;
padding-right: 0;
}
span.title_action {
flex-grow: 1;
margin: 0 0 0 auto;
border-radius: 0 0.35rem 0.35rem 0;
background-color: $light;
padding: 0.2em 0.7em;
@include badge_social_action;
}
}
div.timeline {
width: 100%;
ul {
@@ -125,7 +100,7 @@ div.accompanying_course_work-list {
}
}
&.goal_title li::marker {
color: $sky-blue;
color: $social-issue-color;
}
&.result_list li::marker {
color: $pink;
@@ -133,11 +108,4 @@ div.accompanying_course_work-list {
}
}
.updatedBy {
margin-top: 0.3rem;
font-size: 0.9rem;
font-style: italic;
}
}

View File

@@ -0,0 +1,119 @@
/*
* BADGES PERSON AND THIRDPARTY
*/
span.badge-person,
span.badge-thirdparty {
display: inline-block;
padding: 0 0.5em !important;
background-color: $white;
color: $dark;
border: 1px solid $chill-ll-gray;
border-bottom-width: 2px;
border-bottom-style: solid;
border-radius: 6px;
a {
text-decoration: none;
}
}
span.badge-person {
border-bottom-color: $chill-green;
}
span.badge-thirdparty {
border-bottom-color: shade-color($chill-pink, 10%);
}
/*
* HOUSEHOLD HOLDER MARK
*/
span.fa-holder {
width: 1em;
margin: -10px 0.3em -8px 0;
i:last-child {
font-family: "Open Sans Extrabold";
font-weight: 900;
font-size: 70%;
color: $white;
}
}
/*
* BADGE_TITLE
* Display Title like a badge (with background-colored label)
*/
h2.badge-title {
display: flex;
flex-direction: row;
width: 100%;
color: $dark;
a & { text-decoration: none; } // ?!? keep it ?
span.title_label {
border-radius: 0.35rem 0 0 0.35rem;
color: $white;
font-size: 80%;
padding: 0.5em;
padding-right: 0;
h3 {
margin-bottom: 0.5rem;
}
}
span.title_action {
flex-grow: 1;
margin: 0 0 0 auto;
border-radius: 0 0.35rem 0.35rem 0;
background-color: $light;
padding: 0.2em 1em;
ul.small_in_title {
margin-top: 0.5em;
font-size: 70%;
padding-left: 1rem;
}
}
}
/// badge_title in AccompanyingCourse Work list Page
div.accompanying_course_work-list {
h2.badge-title {
span.title_label {
// Calculate same color then border:groove
background-color: shade-color($social-action-color, 34%);
}
span.title_action {
@include badge_title($social-action-color);
}
}
}
/// badge_title in Activities on resume page
div.activity-list {
h2.badge-title {
span.title_label {
// Calculate same color then border:groove
background-color: shade-color($activity-color, 34%);
h3 {
color: $white;
}
}
span.title_action {
@include badge_title($activity-color);
}
span.title_label {
div.duration {
font-size: 70%;
font-weight: 500;
p {
margin-bottom: 0;
text-align: right;
abbr {
text-decoration: none;
}
}
}
}
}
}

View File

@@ -1,44 +1,39 @@
// Additionnal colors
$sky-blue: #4bafe8;
// mixins variables
$social-issue-color: $sky-blue;
$social-action-color: $orange;
// Calculate same color then border:groove
// origin: $orange, computed: #965028
$social-action-label-color: shade-color($orange, 34%);
///
/// Social Issue mixin
// define visual badge for all social issues
/// Chill badge mixin
// define chill visual badge
///
@mixin badge_social_issue {
@mixin chill_badge($color) {
text-transform: capitalize !important;
font-weight: 500 !important;
border-left: 20px groove $social-issue-color;
border-left: 20px groove $color;
&:before {
content: '\f04b';
font-family: ForkAwesome;
color: $social-issue-color;
content: '\f04b';
color: $color;
}
}
///
/// Social badge mixin
// define visual badge for social issues or social action
///
@mixin badge_social($color) {
@include chill_badge($color);
&:before {
margin: 0 0.3em 0 -0.75em;
}
}
///
/// Social Action mixin
// define visual badge for all social actions
/// Generic mixin for titles like badge
// define visual badge used in title area
///
@mixin badge_social_action {
text-transform: capitalize !important;
font-weight: 500 !important;
border-left: 20px groove $social-action-color;
@mixin badge_title($color) {
@include chill_badge($color);
&:before {
content: '\f04b';
font-family: ForkAwesome;
color: $social-action-color;
margin: 0 0.3em 0 -0.75em;
margin: 0 0.3em 0 -1.05em;
}
}

View File

@@ -8,21 +8,28 @@
span.altname {}
}
/// SOCIAL-ISSUE
&.entity-social-issue {
margin-right: 0.3em;
font-size: 120%;
span.badge {
@include badge_social_issue;
}
}
/// SOCIAL-ACTION
/// SOCIAL-ISSUE AND SOCIAL-ACTION
&.entity-social-issue,
&.entity-social-action {
margin-right: 0.3em;
font-size: 120%;
span.badge {
@include badge_social_action;
// for too long badge in a narrow inside block
text-align: initial;
margin-bottom: 0.2em;
> span {
white-space: normal;
}
}
}
&.entity-social-issue {
span.badge {
@include badge_social($social-issue-color);
}
}
&.entity-social-action {
span.badge {
@include badge_social($social-action-color);
}
}
}

View File

@@ -12,9 +12,9 @@ export default {
<style lang="scss" scoped>
@import 'ChillMainAssets/module/bootstrap/shared';
@import 'ChillPersonAssets/chill/scss/mixins';
@import 'ChillMainAssets/chill/scss/chill_variables';
span.badge {
@include badge_social_issue;
@include badge_social($social-issue-color);
font-size: 95%;
margin-bottom: 5px;
margin-right: 1em;

View File

@@ -84,10 +84,10 @@ export default {
<style lang="scss">
@import 'ChillMainAssets/module/bootstrap/shared';
@import 'ChillPersonAssets/chill/scss/mixins';
@import 'ChillMainAssets/chill/scss/chill_variables';
div#accompanying-course {
span.multiselect__tag {
@include badge_social_issue;
@include badge_social($social-issue-color);
background: $chill-l-gray;
color: $dark;
}

View File

@@ -1,37 +1,102 @@
<template>
<div class="container">
<span class="name">
{{ item.result.text }}
<div class="container tpartycontainer">
<div class="tparty-identification">
<span class="name">
{{ item.result.text }}
</span>
<span class="location">
<template v-if="hasAddress">
{{ getAddress.text }} -
{{ getAddress.postcode.name }}
</template>
</span>
</div>
<div class="tpartyparent" v-if="hasParent">
<span class="name">
> {{ item.result.parent.text }}
</span>
</div>
</div>
<div class="right_actions">
<span class="badge bg-thirdparty-child" v-if="item.result.kind == 'child'">
{{ $t('thirdparty.child')}}
</span>
<span class="badge bg-thirdparty-company" v-else-if="item.result.kind == 'company'">
{{ $t('thirdparty.company')}}
</span>
<span class="location">
{{ item.result.address.text }} -
{{ item.result.address.postcode.name }}
<span class="badge bg-thirdparty-contact" v-else="item.result.kind == 'contact'">
{{ $t('thirdparty.contact')}}
</span>
</div>
<div class="right_actions">
<span class="badge rounded-pill bg-secondary" :title="item.key">
{{ $t('item.type_thirdparty') }}
</span>
<on-the-fly
type="thirdparty"
v-bind:id="item.result.id"
action="show">
type="thirdparty"
v-bind:id="item.result.id"
action="show">
</on-the-fly>
</div>
</div>
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
const i18n = {
messages: {
fr: {
thirdparty: {
contact: "Personne physique",
company: "Personne morale",
child: "Personne de contact"
}
}
}
};
export default {
name: 'SuggestionThirdParty',
components: {
OnTheFly
},
props: ['item']
props: ['item'],
i18n,
computed: {
hasAddress() {
if (this.$props.item.result.address !== null) {
return true;
}
if (this.$props.item.result.parent !== null) {
this.$props.item.result.parent.address !== null;
}
},
hasParent() {
return this.$props.item.result.parent !== null;
},
getAddress() {
if (this.$props.item.result.address !== null) {
return this.$props.item.result.address;
}
if (this.$props.item.result.parent.address !== null) {
return this.$props.item.result.parent.address;
}
return null;
}
}
}
</script>
<style lang="scss" scoped>
.tpartycontainer {
.tpartyparent {
.name {
font-weight: bold;
font-variant: all-small-caps;
}
}
}
</style>

View File

@@ -71,7 +71,7 @@
<div class="col-md-6 location mb-5">
{% if accompanyingCourse.locationStatus == 'person' %}
<h5>{{ 'This course is located by'|trans }}</h5>
<h2>{{ 'This course is located by'|trans }}</h2>
<h4>{{ accompanyingCourse.personLocation|chill_entity_render_string }}</h4>
{% elseif accompanyingCourse.locationStatus == 'address' %}
<h4>{{ 'This course has a temporarily location'|trans }}</h4>
@@ -82,97 +82,9 @@
{% endif %}
</div>
{# DISABLED
<h1>{{ 'Resume Accompanying Course'|trans }}</h1>
<div class="associated-persons mb-5">
<h2 class="mb-3">{{ 'Associated peoples'|trans }}</h2>
<div class="flex-table mb-3">
{% for participation in accompanyingCourse.participations %}
{% if participation.enddate is null %}
<div class="item-bloc">
{{ participation.person|chill_entity_render_box({
'render': 'bloc', 'addLink': false, 'addInfo': true, 'addAltNames': false,
'customButtons': { 'before': _self.button_person(participation.person) }
}) }}
</div>
{% endif %}
{% endfor %}
</div>
</div>
<div class="location mb-5">
<h2 class="mb-3">{{ 'Accompanying course location'|trans }}</h2>
{% if accompanyingCourse.locationStatus == 'person' %}
<p>{{ 'This course is located by'|trans }}
<b>{{ accompanyingCourse.personLocation|chill_entity_render_string }}</b>
</p>
{% elseif accompanyingCourse.locationStatus == 'address' %}
<p>{{ 'This course has a temporarily location'|trans }}</p>
{% endif %}
{% if accompanyingCourse.locationStatus != 'none' %}
<div class="flex-table">
<div class="item-bloc">
{{ accompanyingCourse.location|chill_entity_render_box }}
</div>
</div>
{% endif %}
</div>
<div class="requestor mb-5">
<h2 class="mb-3">{{ 'Requestor'|trans }}</h2>
{% if accompanyingCourse.requestorPerson is not empty %}
{% set requestor = accompanyingCourse.requestorPerson %}
{% set info = true %}
{% elseif accompanyingCourse.requestor is not empty %}
{% set requestor = accompanyingCourse.requestorThirdParty %}
{% set info = false %}
{% endif %}
{% if accompanyingCourse.requestor == null %}
<p class="chill-no-data-statement">{{ 'Any requestor to this accompanying course'|trans }}</p>
{% else %}
<div class="flex-bloc row row-cols-1 g-0">
<div class="item-bloc col">
{{ requestor|chill_entity_render_box({
'render': 'bloc', 'addLink': false, 'addEntity': true, 'addInfo': info, 'addAltNames': false
}) }}
</div>
</div>
{% endif %}
</div>
<div class="resources mb-5">
<h2 class="mb-3">{{ 'Resources'|trans }}</h2>
{% if accompanyingCourse.resources|length == 0 %}
<p class="chill-no-data-statement">{{ 'Any resource for this accompanying course'|trans }}</p>
{% else %}
<div class="flex-bloc row row-cols-1 row-cols-sm-2 row-cols-xl-3 row-cols-xxl-4 g-0">
{% for r in accompanyingCourse.resources %}
<div class="item-bloc col">
{% if r.person %}
{{ r.person|chill_entity_render_box({
'render': 'bloc', 'addLink': false, 'addEntity': true, 'addInfo': true, 'addAltNames': false
}) }}
{% endif %}
{% if r.thirdParty %}
{{ r.thirdParty|chill_entity_render_box({
'render': 'bloc', 'addLink': false, 'addEntity': true, 'addInfo': false
}) }}
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
</div>
#}
<div class="mt-5"></div>
<div class="social-actions mb-5">
<h2 class="mb-3">{{ 'Social actions'|trans }}</h2>
{% include 'ChillPersonBundle:AccompanyingCourseWork:list_by_accompanying_period.html.twig' with {'buttonText': false } %}
<h2 class="mb-3">{{ 'Last social actions'|trans }}</h2>
{% include 'ChillPersonBundle:AccompanyingCourseWork:list_recent_by_accompanying_period.html.twig' with {'buttonText': false } %}
</div>
{% block contentActivity %}
@@ -189,9 +101,9 @@
{% set accompanying_course_id = accompanyingCourse.id %}
{% endif %}
<h2>{{ 'Activity list' |trans }}</h2>
<h2 class="mb-3">{{ 'Last activities' |trans }}</h2>
{% include 'ChillActivityBundle:Activity:list.html.twig' with { 'context': 'accompanyingCourse', 'no_action': true } %}
{% include 'ChillActivityBundle:Activity:list_recent.html.twig' with { 'context': 'accompanyingCourse', 'no_action': true } %}
</div>
{% endblock %}

View File

@@ -10,7 +10,7 @@
<div class="item-bloc">
<div class="item-row">
<h2 class="title">
<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>
</h2>

View File

@@ -0,0 +1,32 @@
{% 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 }}
<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') }}
</li>
<li>
<abbr title="{{ 'Last updated by'|trans }}">{{ 'Last updated by'|trans ~ ' : ' }}</abbr>
{{ w.updatedBy|chill_entity_render_box }}, {{ w.updatedAt|format_datetime('short', 'short') }}
</li>
</ul>
</span>
</h2>
{% endfor %}
</div>

View File

@@ -58,11 +58,10 @@
</div>
<div class="item-row separator">
<div class="wrap-list">
{% if accompanying_period.requestorPerson is not null or accompanying_period.requestorThirdParty is not null %}
<div class="wl-row">
<div class="wl-col title"><h3>{{ 'Requestor'|trans }}</h3></div>
<div class="wl-col title"><h3>{{ 'Requestor'|trans({'gender': null }) }}</h3></div>
<div class="wl-col list">
{% if accompanying_period.requestorPerson is not null %}
<span class="wl-item badge-person">{{ accompanying_period.requestorPerson|chill_entity_render_string }}</span>

View File

@@ -61,7 +61,7 @@
</table>
{% endif %}
{{ form_start(form) }}
{{ form_start(form, {'attr' : {'id' : 'create-form'}}) }}
{{ form_row(form.lastName, { 'label' : 'Last name'|trans }) }}
@@ -104,3 +104,7 @@
</div>
{% endblock content %}
{% block js %}
{{ encore_entry_script_tags('mod_disablebuttons') }}
{% endblock js %}

View File

@@ -18,12 +18,12 @@
{% set activeRouteKey = '' %}
{% block title %}{{ 'Update details for %name%'|trans({ '%name%': person.firstName|capitalize ~ ' ' ~ person.lastName } )|capitalize }}{% endblock %}
{% block title %}{{ 'Update details for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %}
{% block personcontent %}
<div class="person-edit">
<h1>{{ 'Update details for %name%'|trans({ '%name%': person.firstName|capitalize ~ ' ' ~ person.lastName|capitalize } ) }}</h1>
<h1>{{ block('title') }}</h1>
{% form_theme form '@ChillMain/Form/fields.html.twig' %}
{{ form_start(form) }}

View File

@@ -23,8 +23,7 @@ This view should receive those arguments:
- person
#}
{% block title %}{{ 'Person details'|trans|capitalize ~ ' ' ~ person.firstName|capitalize ~
' ' ~ person.lastName }}{% endblock %}
{% block title %}{{ 'Person details'|trans|capitalize ~ ' ' ~ person|chill_entity_render_string }}{% endblock %}
{#
we define variables to include an edit form repeated multiple time across
the page

View File

@@ -26,7 +26,7 @@ class SearchPersonApiProvider implements SearchApiInterface
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int".
")", [ $pattern, $pattern ])
->setFromClause("chill_person_person AS person")
->setWhereClause("LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
->setWhereClauses("LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ", [ $pattern, $pattern ])
;

View File

@@ -177,9 +177,11 @@ class PersonControllerCreateTest extends WebTestCase
$this->assertTrue($form->has(self::CENTER_INPUT),
'The page contains a "center" input');
$centerInput = $form->get(self::CENTER_INPUT);
/*
$availableValues = $centerInput->availableOptionValues();
$lastCenterInputValue = end($availableValues);
$centerInput->setValue($lastCenterInputValue);
*/
$client->submit($form);
@@ -205,7 +207,7 @@ class PersonControllerCreateTest extends WebTestCase
$form = $this->fillAValidCreationForm($form, 'Charline', 'dd');
$client->submit($form);
$this->assertContains('Depardieu', $client->getCrawler()->text(),
$this->assertContains('DEPARDIEU', $client->getCrawler()->text(),
"check that the page has detected the lastname of a person existing in database");
//inversion
@@ -213,7 +215,7 @@ class PersonControllerCreateTest extends WebTestCase
$form = $this->fillAValidCreationForm($form, 'dd', 'Charline');
$client->submit($form);
$this->assertContains('Depardieu', $client->getCrawler()->text(),
$this->assertContains('DEPARDIEU', $client->getCrawler()->text(),
"check that the page has detected the lastname of a person existing in database");
}

View File

@@ -30,38 +30,38 @@ class PersonControllerViewTest extends WebTestCase
{
/** @var \Doctrine\ORM\EntityManagerInterface The entity manager */
private $em;
/** @var Person A person used on which to run the test */
private $person;
/** @var String The url to view the person details */
private $viewUrl;
public function setUp()
{
static::bootKernel();
$this->em = static::$kernel->getContainer()
->get('doctrine.orm.entity_manager');
$center = $this->em->getRepository('ChillMainBundle:Center')
->findOneBy(array('name' => 'Center A'));
$this->person = (new Person())
->setLastName("Tested Person")
->setFirstName("Réginald")
->setCenter($center)
->setGender(Person::MALE_GENDER);
$this->em->persist($this->person);
$this->em->flush();
$this->viewUrl = '/en/person/'.$this->person->getId().'/general';
}
/**
* Test if the view page is accessible
*
*
* @group configurable_fields
*/
public function testViewPerson()
@@ -70,20 +70,20 @@ class PersonControllerViewTest extends WebTestCase
'PHP_AUTH_USER' => 'center a_social',
'PHP_AUTH_PW' => 'password',
));
$crawler = $client->request('GET', $this->viewUrl);
$response = $client->getResponse();
$this->assertTrue($response->isSuccessful());
$this->assertGreaterThan(0, $crawler->filter('html:contains("Tested Person")')->count());
$this->assertGreaterThan(0, $crawler->filter('html:contains("TESTED PERSON")')->count());
$this->assertGreaterThan(0, $crawler->filter('html:contains("Réginald")')->count());
$this->assertContains('Email addresses', $crawler->text());
$this->assertContains('Phonenumber', $crawler->text());
$this->assertContains('Langues parlées', $crawler->text());
$this->assertContains(/* Etat */ 'civil', $crawler->text());
}
/**
* Test if the view page of a given person is not accessible for a user
* of another center of the person
@@ -94,26 +94,26 @@ class PersonControllerViewTest extends WebTestCase
'PHP_AUTH_USER' => 'center b_social',
'PHP_AUTH_PW' => 'password',
));
$client->request('GET', $this->viewUrl);
$this->assertEquals(403, $client->getResponse()->getStatusCode(),
"The view page of a person of a center A must not be accessible for user of center B");
}
/**
* Reload the person from the db
*/
protected function refreshPerson()
protected function refreshPerson()
{
$this->person = $this->em->getRepository('ChillPersonBundle:Person')
->find($this->person->getId());
}
public function tearDown()
{
$this->refreshPerson();
$this->em->remove($this->person);
$this->em->flush();
}
}

View File

@@ -0,0 +1,61 @@
<?php
use PHPUnit\Framework\TestCase;
use Chill\PersonBundle\EventListener\PersonEventListener;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use Doctrine\Persistence\Event\LifecycleEventArgs;
class PersonCreateEventTest extends TestCase
{
/**
* @dataProvider generateNames
*/
public function testOnPrePersist($firstname, $firstnameExpected, $lastname, $lastnameExpected)
{
$listener = new PersonEventListener();
$person = new Person();
$person->setFirstName($firstname);
$person->setLastName($lastname);
$listener->prePersistPerson($person);
$this->assertEquals($firstnameExpected, $person->getFirstName());
$this->assertEquals($lastnameExpected, $person->getLastName());
}
/**
* @dataProvider generateAltNames
*/
public function testAltNamesOnPrePersist($altname, $altnameExpected)
{
$listener = new PersonEventListener();
$personAltname = new PersonAltName();
$personAltname->setLabel($altname);
$listener->prePersistAltName($personAltname);
$this->assertEquals($altnameExpected, $personAltname->getLabel());
}
public function generateNames(): iterator
{
yield ['émelie-marie', 'Émelie-Marie', 'lenaerts', 'LENAERTS'];
yield ['jean-marie', 'Jean-Marie', 'lenaerts', 'LENAERTS'];
yield ['vinCENT', 'Vincent', 'fastré', 'FASTRÉ'];
yield ['Vincent', 'Vincent', 'van Gogh', 'VAN GOGH'];
yield ['André marie', 'André Marie', 'Bah', 'BAH'];
}
public function generateAltNames(): iterator
{
yield ['vinCENT', 'VINCENT'];
yield ['jean-marie', 'JEAN-MARIE'];
yield ['fastré', 'FASTRÉ'];
yield ['émile', 'ÉMILE'];
}
}

View File

@@ -26,7 +26,6 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* Test Person search
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class PersonSearchTest extends WebTestCase
{
@@ -38,7 +37,7 @@ class PersonSearchTest extends WebTestCase
'q' => '@person Depardieu'
));
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text());
$this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
}
public function testExpectedNamed()
@@ -49,61 +48,61 @@ class PersonSearchTest extends WebTestCase
'q' => '@person Depardieu', 'name' => 'person_regular'
));
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text());
$this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
}
public function testSearchByLastName()
{
$crawler = $this->generateCrawlerForSearch('@person lastname:Depardieu');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text());
$this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
}
public function testSearchByFirstNameLower()
{
$crawler = $this->generateCrawlerForSearch('@person firstname:Gérard');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text());
$this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
}
public function testSearchByFirstNamePartim()
{
$crawler = $this->generateCrawlerForSearch('@person firstname:Ger');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text());
$this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
}
public function testLastNameAccentued()
{
$crawlerSpecial = $this->generateCrawlerForSearch('@person lastname:manço');
$this->assertRegExp('/Manço/', $crawlerSpecial->filter('.list-with-period')->text());
$this->assertRegExp('/MANÇO/', $crawlerSpecial->filter('.list-with-period')->text());
$crawlerNoSpecial = $this->generateCrawlerForSearch('@person lastname:manco');
$this->assertRegExp('/Manço/', $crawlerNoSpecial->filter('.list-with-period')->text());
$this->assertRegExp('/MANÇO/', $crawlerNoSpecial->filter('.list-with-period')->text());
}
public function testSearchByFirstName()
{
$crawler = $this->generateCrawlerForSearch('@person firstname:Jean');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text());
$this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
}
public function testSearchByFirstNameLower2()
{
$crawler = $this->generateCrawlerForSearch('@person firstname:jean');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text());
$this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
}
public function testSearchByFirstNamePartim2()
{
$crawler = $this->generateCrawlerForSearch('@person firstname:ean');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text());
$this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
}
public function testSearchByFirstNameAccented()
@@ -154,7 +153,7 @@ class PersonSearchTest extends WebTestCase
$crawler = $this->generateCrawlerForSearch('@person birthdate:1948-12-27 lastname:(Van Snick)');
$this->assertRegExp('/Bart/', $crawler->filter('.list-with-period')->text());
$this->assertNotRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text());
$this->assertNotRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
}
public function testSearchCombineGenderAndLastName()
@@ -181,12 +180,12 @@ class PersonSearchTest extends WebTestCase
$this->markTestSkipped("skipped until adapted to new fixtures");
$crawlerSpecial = $this->generateCrawlerForSearch('@person manço');
$this->assertRegExp('/Manço/', $crawlerSpecial->filter('.list-with-period')->text());
$this->assertRegExp('/MANÇO/', $crawlerSpecial->filter('.list-with-period')->text());
$crawlerNoSpecial = $this->generateCrawlerForSearch('@person manco');
$this->assertRegExp('/Manço/', $crawlerNoSpecial->filter('.list-with-period')->text());
$this->assertRegExp('/MANÇO/', $crawlerNoSpecial->filter('.list-with-period')->text());
$crawlerSpecial = $this->generateCrawlerForSearch('@person Étienne');
@@ -206,10 +205,10 @@ class PersonSearchTest extends WebTestCase
$crawlerCanSee = $this->generateCrawlerForSearch('Gérard', 'center a_social');
$crawlerCannotSee = $this->generateCrawlerForSearch('Gérard', 'center b_social');
$this->assertRegExp('/Depardieu/', $crawlerCanSee->text(),
$this->assertRegExp('/DEPARDIEU/', $crawlerCanSee->text(),
'center a_social may see "Depardieu" in center a');
$this->assertNotRegExp('/Depardieu/', $crawlerCannotSee->text(),
'center b_social may see "Depardieu" in center b');
$this->assertNotRegExp('/DEPARDIEU/', $crawlerCannotSee->text(),
'center b_social may not see "Depardieu" in center b');
}

View File

@@ -0,0 +1,14 @@
services:
Chill\PersonBundle\EventListener\PersonEventListener:
autoconfigure: true
tags:
-
name: 'doctrine.orm.entity_listener'
event: 'prePersist'
entity: 'Chill\PersonBundle\Entity\Person'
method: 'prePersistPerson'
-
name: 'doctrine.orm.entity_listener'
event: 'prePersist'
entity: 'Chill\PersonBundle\Entity\PersonAltName'
method: 'prePersistAltName'

View File

@@ -191,6 +191,7 @@ Associated peoples: Usagers concernés
Resources: Interlocuteurs privilégiés
Any requestor to this accompanying course: Aucun demandeur pour ce parcours
Social actions: Actions d'accompagnement
Last social actions: Les dernières actions d'accompagnement
Social issues: Problématiques sociales
Last events on accompanying course: Dernières actions de suivi
Edit & activate accompanying course: Modifier et valider
@@ -380,7 +381,7 @@ Create Accompanying Course: Créer un nouveau parcours
Drop Accompanying Course: Supprimer le parcours
This course is located at a temporarily address. You should locate this course to an user: Le parcours est localisé à une adresse temporaire. Il devrait être localisé auprès d'une personne concernée.
Accompanying course location: Localisation du parcours
This course is located by: Parcours localisé auprès de
This course is located by: Localisé auprès de
This course has a temporarily location: Localisation temporaire
Choose a person to locate by: Localiser auprès d'un usager concerné
Associate at least one member with an household, and set an address to this household: Associez au moins un membre du parcours à un ménage, et indiquez une adresse à ce ménage.

View File

@@ -1,20 +1,20 @@
<?php
/*
* Chill is a software for social workers
* Chill is a software for social workers
*
* 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/>.
*/
@@ -35,11 +35,9 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
{
return 15001;
}
public function load(ObjectManager $manager)
{
echo "loading CustomField...\n";
$cFTypes = [
array('type' => 'text', 'options' => array('maxLength' => '255')),
array('type' => 'text', 'options' => array('maxLength' => '1000')),
@@ -78,7 +76,6 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
];
for($i=0; $i <= 25; $i++) {
echo "CustomField {$i}\n";
$cFType = $cFTypes[rand(0,sizeof($cFTypes) - 1)];
$customField = (new CustomField())
@@ -92,17 +89,17 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($customField);
}
$this->createExpectedFields($manager);
$manager->flush();
}
private function createExpectedFields(ObjectManager $manager)
{
//report logement
$reportLogement = $this->getReference('cf_group_report_logement');
$houseTitle = (new CustomField())
->setSlug('house_title')
->setType('title')
@@ -112,7 +109,7 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
->setCustomFieldsGroup($reportLogement)
;
$manager->persist($houseTitle);
$hasLogement = (new CustomField())
->setSlug('has_logement')
->setName(array('fr' => 'Logement actuel'))
@@ -143,13 +140,13 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
'active' => true
)
]
))
->setOrdering(20)
->setCustomFieldsGroup($reportLogement)
;
$manager->persist($hasLogement);
$descriptionLogement = (new CustomField())
->setSlug('house-desc')
->setName(array('fr' => 'Plaintes éventuelles sur le logement'))
@@ -159,11 +156,11 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
->setCustomFieldsGroup($reportLogement)
;
$manager->persist($descriptionLogement);
//report problems
$reportEducation = $this->getReference('cf_group_report_education');
$title = (new CustomField())
->setSlug('title')
->setType('title')
@@ -173,7 +170,7 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
->setCustomFieldsGroup($reportEducation)
;
$manager->persist($title);
$educationLevel = (new CustomField())
->setSlug('level')
->setName(array('fr' => 'Niveau du plus haut diplôme'))
@@ -209,14 +206,14 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
'active' => true
)
]
))
->setOrdering(20)
->setCustomFieldsGroup($reportEducation)
;
$manager->persist($educationLevel);
}
}
}

View File

@@ -3,17 +3,17 @@
/*
* Chill is a suite of a modules, Chill is a software for social workers
* 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/>.
*/
@@ -38,43 +38,43 @@ use Chill\MainBundle\DataFixtures\ORM\LoadScopes;
class LoadReports extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface
{
use \Symfony\Component\DependencyInjection\ContainerAwareTrait;
/**
*
* @var \Faker\Generator
* @var \Faker\Generator
*/
private $faker;
public function __construct()
{
$this->faker = FakerFactory::create('fr_FR');
}
public function getOrder()
{
return 15002;
}
public function load(ObjectManager $manager)
{
$this->createExpected($manager);
//create random 2 times, to allow multiple report on some people
$this->createRandom($manager, 90);
$this->createRandom($manager, 30);
$manager->flush();
}
private function createRandom(ObjectManager $manager, $percentage)
{
$people = $this->getPeopleRandom($percentage);
foreach ($people as $person) {
//create a report, set logement or education report
$report = (new Report())
->setPerson($person)
->setCFGroup(rand(0,10) > 5 ?
->setCFGroup(rand(0,10) > 5 ?
$this->getReference('cf_group_report_logement') :
$this->getReference('cf_group_report_education')
)
@@ -84,27 +84,31 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
$manager->persist($report);
}
}
private function createExpected(ObjectManager $manager)
{
$charline = $this->container->get('doctrine.orm.entity_manager')
->getRepository('ChillPersonBundle:Person')
->findOneBy(array('firstName' => 'Charline', 'lastName' => 'Depardieu'))
->findOneBy(array('firstName' => 'Charline', 'lastName' => 'DEPARDIEU'))
;
$report = (new Report())
if (NULL !== $charline) {
$report = (new Report())
->setPerson($charline)
->setCFGroup($this->getReference('cf_group_report_logement'))
->setDate(new \DateTime('2015-01-05'))
->setScope($this->getReference('scope_social'))
;
$this->fillReport($report);
$manager->persist($report);
;
$this->fillReport($report);
$manager->persist($report);
} else {
print("WARNING: Charline DEPARDIEU not found in database");
}
}
/**
*
*
* @return \Chill\MainBundle\Entity\Scope
*/
private function getScopeRandom()
@@ -112,14 +116,14 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
$ref = LoadScopes::$references[array_rand(LoadScopes::$references)];
return $this->getReference($ref);
}
private function getPeopleRandom($percentage)
{
$people = $this->container->get('doctrine.orm.entity_manager')
->getRepository('ChillPersonBundle:Person')
->findAll()
;
//keep only a part ($percentage) of the people
$selectedPeople = array();
foreach($people as $person) {
@@ -127,10 +131,10 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
$selectedPeople[] = $person;
}
}
return $selectedPeople;
}
private function fillReport(Report $report)
{
//setUser
@@ -138,7 +142,7 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
$report->setUser(
$this->getReference($usernameRef)
);
//set date if null
if ($report->getDate() === NULL) {
//set date. 30% of the dates are 2015-05-01
@@ -148,9 +152,9 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
} else {
$report->setDate($this->faker->dateTimeBetween('-1 year', 'now')
->setTime(0, 0, 0));
}
}
}
//fill data
$datas = array();
foreach ($report->getCFGroup()->getCustomFields() as $field) {
@@ -167,66 +171,66 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
}
}
$report->setCFData($datas);
return $report;
}
/**
* pick a random choice
*
*
* @param CustomField $field
* @return string[]|string the array of slug if multiple, a single slug otherwise
*/
private function getRandomChoice(CustomField $field)
private function getRandomChoice(CustomField $field)
{
$choices = $field->getOptions()['choices'];
$multiple = $field->getOptions()['multiple'];
$other = $field->getOptions()['other'];
//add other if allowed
if($other) {
$choices[] = array('slug' => '_other');
}
//initialize results
$picked = array();
if ($multiple) {
$numberSelected = rand(1, count($choices) -1);
for ($i = 0; $i < $numberSelected; $i++) {
$picked[] = $this->pickChoice($choices);
}
if ($other) {
$result = array("_other" => NULL, "_choices" => $picked);
if (in_array('_other', $picked)) {
$result['_other'] = $this->faker->realText(70);
}
return $result;
}
} else {
$picked = $this->pickChoice($choices);
if ($other) {
$result = array('_other' => NULL, '_choices' => $picked);
if ($picked === '_other') {
$result['_other'] = $this->faker->realText(70);
}
return $result;
}
}
}
/**
* pick a choice within a 'choices' options (for choice type)
*
*
* @param array $choices
* @return the slug of the selected choice
*/
@@ -234,7 +238,7 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
{
return $choices[array_rand($choices)]['slug'];
}
}

View File

@@ -2,6 +2,7 @@
namespace Chill\ThirdPartyBundle;
use Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeProviderInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Chill\ThirdPartyBundle\DependencyInjection\CompilerPass\ThirdPartyTypeCompilerPass;
@@ -10,6 +11,8 @@ class ChillThirdPartyBundle extends Bundle
public function build(\Symfony\Component\DependencyInjection\ContainerBuilder $container)
{
parent::build($container);
$container->registerForAutoconfiguration(ThirdPartyTypeProviderInterface::class)
->addTag('chill_3party.provider');
$container->addCompilerPass(new ThirdPartyTypeCompilerPass());
}

View File

@@ -2,7 +2,18 @@
namespace Chill\ThirdPartyBundle\Controller;
use Chill\MainBundle\CRUD\Controller\AbstractCRUDController;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\ThirdPartyBundle\Repository\ThirdPartyACLAwareRepositoryInterface;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use http\Exception\RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
@@ -16,12 +27,7 @@ use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Security\Core\Role\Role;
use Chill\MainBundle\Pagination\PaginatorFactory;
/**
* Routes for operations on ThirdParties.
*
* @Route("/{_locale}/thirdparty/thirdparty")
*/
class ThirdPartyController extends Controller
final class ThirdPartyController extends CRUDController
{
/**
*
@@ -41,145 +47,88 @@ class ThirdPartyController extends Controller
*/
protected $paginatorFactory;
protected ThirdPartyACLAwareRepositoryInterface $thirdPartyACLAwareRepository;
protected RequestStack $requestStack;
public function __construct(
AuthorizationHelper $authorizationHelper,
TranslatorInterface $translator,
PaginatorFactory $paginatorFactory
PaginatorFactory $paginatorFactory,
RequestStack $requestStack,
ThirdPartyACLAwareRepositoryInterface $thirdPartyACLAwareRepository
) {
$this->authorizationHelper = $authorizationHelper;
$this->translator = $translator;
$this->paginatorFactory = $paginatorFactory;
$this->requestStack = $requestStack;
$this->thirdPartyACLAwareRepository = $thirdPartyACLAwareRepository;
}
/**
* @Route("/index", name="chill_3party_3party_index")
*/
public function indexAction()
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::SHOW);
$repository = $this->getDoctrine()->getManager()
->getRepository(ThirdParty::class);
$nbThirdParties = $repository->count([]); //$repository->countByMemberOfCenters($centers);
$pagination = $this->paginatorFactory->create($nbThirdParties);
$thirdParties = $repository->findAll();
return $this->render('ChillThirdPartyBundle:ThirdParty:index.html.twig', array(
'third_parties' => $thirdParties,
'pagination' => $pagination
));
}
/**
* @Route("/new", name="chill_3party_3party_new")
*/
public function newAction(Request $request)
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::CREATE);
$centers = [];
$thirdParty = new ThirdParty();
$thirdParty->setCenters(new ArrayCollection($centers));
$form = $this->createForm(ThirdPartyType::class, $thirdParty, [
'usage' => 'create'
]);
$form->add('submit', SubmitType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($thirdParty);
$em->flush();
$this->addFlash('success',
$this->translator->trans("Third party created")
);
return $this->redirectToRoute('chill_3party_3party_show', [
'thirdparty_id' => $thirdParty->getId()
]);
} elseif ($form->isSubmitted()) {
$msg = $this->translator->trans('This form contains errors');
$this->addFlash('error', $msg);
if (NULL === $filterOrder){
throw new \LogicException('filterOrder should not be null');
}
return $this->render('@ChillThirdParty/ThirdParty/new.html.twig', [
'form' => $form->createView(),
'thirdParty' => $thirdParty
]);
return $this->thirdPartyACLAwareRepository->countThirdParties(ThirdPartyVoter::SHOW,
$filterOrder->getQueryString());
}
/**
* @Route("/{thirdparty_id}/update", name="chill_3party_3party_update")
* @ParamConverter("thirdParty", options={"id": "thirdparty_id"})
*/
public function updateAction(ThirdParty $thirdParty, Request $request)
protected function getQueryResult(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, ?FilterOrderHelper $filterOrder = null)
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::CREATE);
return $this->thirdPartyACLAwareRepository
->listThirdParties(ThirdPartyVoter::SHOW, $filterOrder->getQueryString(), ['name' => 'ASC'], $paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber());
}
$repository = $this->getDoctrine()->getManager()
->getRepository(ThirdParty::class);
$centers = $repository->findAll();
// we want to keep centers the users has no access to. So we will add them
// later if they are removed. (this is a ugly hack but it will works
$centersAssociatedNotForUsers = \array_diff(
$thirdParty->getCenters()->toArray(),
$centers);
$form = $this->createForm(ThirdPartyType::class, $thirdParty, [
'usage' => 'create'
]);
$form->add('submit', SubmitType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// re-add centers the user has no accesses:
foreach ($centersAssociatedNotForUsers as $c) {
$thirdParty->addCenter($c);
protected function onPostCheckACL($action, Request $request, $entity): ?Response
{
if ('edit' === $action || 'view' === $action) {
if ($entity->isChild()) {
throw $this->createAccessDeniedException();
}
$em = $this->getDoctrine()->getManager();
$em->flush();
$this->addFlash('success',
$this->translator->trans("Third party updated")
);
return $this->redirectToRoute('chill_3party_3party_show', [
'thirdparty_id' => $thirdParty->getId()
]);
} elseif ($form->isSubmitted()) {
$msg = $this->translator->trans('This form contains errors');
$this->addFlash('error', $msg);
}
return $this->render('@ChillThirdParty/ThirdParty/update.html.twig', [
'form' => $form->createView(),
'thirdParty' => $thirdParty
]);
if ('new' === $action) {
if (!$request->query->has('kind')) {
return $this->render('@ChillThirdParty/ThirdParty/new_pick_kind.html.twig');
} else {
$kind = $request->query->getAlpha('kind', '');
if (!(ThirdParty::KIND_COMPANY === $kind || ThirdParty::KIND_CONTACT === $kind)) {
throw new BadRequestHttpException('This kind is not supported: '.$kind);
}
$entity->setKind($kind);
}
}
return null;
}
/**
* @Route("/{thirdparty_id}/show", name="chill_3party_3party_show")
* @ParamConverter("thirdParty", options={"id": "thirdparty_id"})
*/
public function showAction(ThirdParty $thirdParty, Request $request)
protected function createFormFor(string $action, $entity, string $formClass = null, array $formOptions = []): FormInterface
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::SHOW, $thirdParty);
if ('new' === $action) {
return parent::createFormFor($action, $entity, $formClass, \array_merge(
$formOptions, [ 'kind' => $this->requestStack->getCurrentRequest()->query->getAlpha('kind')]
));
} elseif ('edit' === $action) {
return parent::createFormFor($action, $entity, $formClass, \array_merge(
$formOptions, [ 'kind' => $entity->getKind()]
));
}
return $this->render('@ChillThirdParty/ThirdParty/show.html.twig', [
'thirdParty' => $thirdParty
]);
return parent::createFormFor($action, $entity, $formClass, $formOptions);
}
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
{
return $this->getFilterOrderHelperFactory()
->create(self::class)
->addSearchBox(['name', 'company_name', 'acronym'])
//->addToggle('only-active', [])
// ->addOrderBy()
->build();
}
}

View File

@@ -21,13 +21,14 @@ class LoadThirdParty extends Fixture Implements DependentFixtureInterface
$thirdParties = $this->getThirdParties()->getObjects();
foreach ($thirdParties as $name => $thirdParty) {
if ('a' === $name[0]) {
if ('a' === $name[0]) {
// this is an address
continue;
}
$thirdParty->setCreatedAt(new \DateTimeImmutable('today'));
foreach ($this->getCenters() as $center) {
$thirdParty->addCenter($center);
$thirdParty->addCenter($center);
}
$manager->persist($thirdParty);
@@ -38,7 +39,7 @@ class LoadThirdParty extends Fixture Implements DependentFixtureInterface
private function getCenters(): \Iterator
{
$references = \array_map(function($a) { return $a['ref']; },
$references = \array_map(function($a) { return $a['ref']; },
LoadCenters::$centers);
$number = random_int(1, count($references));

View File

@@ -1,46 +0,0 @@
<?php
namespace Chill\ThirdPartyBundle\DataFixtures\ORM;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCivility;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
/**
* Class LoadThirdPartyCivility
* @package Chill\ThirdPartyBundle\DataFixtures\ORM
* @author Mathieu Jaumotte mathieu.jaumotte@champs-libres.coop
*/
class LoadThirdPartyCivility extends Fixture implements FixtureGroupInterface
{
public static function getGroups(): array
{
return ['thirdparty_civilities'];
}
public function load(ObjectManager $manager): void
{
$civilities = [
['name' => ['fr' => "Monsieur" ]],
['name' => ['fr' => "Madame" ]],
['name' => ['fr' => "Docteur" ]],
['name' => ['fr' => "Professeur" ]],
['name' => ['fr' => "Madame la Directrice" ]],
['name' => ['fr' => "Monsieur le Directeur" ]],
['name' => ['fr' => "Madame la Maire" ]],
['name' => ['fr' => "Monsieur le Maire" ]],
['name' => ['fr' => "Maître" ]],
];
foreach ( $civilities as $val) {
print "Creating thirdparty civility : " . $val['name']['fr'] . "\n";
$civility = (new ThirdPartyCivility())
->setName($val['name'])
->setActive(true);
$manager->persist($civility);
}
$manager->flush();
}
}

View File

@@ -2,6 +2,9 @@
namespace Chill\ThirdPartyBundle\DependencyInjection;
use Chill\ThirdPartyBundle\Controller\ThirdPartyController;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Form\ThirdPartyType;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@@ -37,6 +40,7 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte
$loader->load('services/menu.yaml');
$loader->load('services/fixtures.yaml');
$loader->load('services/serializer.yaml');
$loader->load('services/repository.yaml');
}
public function prepend(ContainerBuilder $container)
@@ -54,6 +58,34 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte
'@ChillThirdPartyBundle/config/routes.yaml'
)
],
'cruds' => [
[
'class' => ThirdParty::class,
'controller' => ThirdPartyController::class,
'name' => '3party_3party',
'base_path' => '/3party/3party',
'form_class' => ThirdPartyType::class,
'actions' => [
'index' => [
'template' => '@ChillThirdParty/ThirdParty/index.html.twig',
'role' => ThirdPartyVoter::SHOW,
],
'new' => [
'template' => '@ChillThirdParty/ThirdParty/new.html.twig',
'role' => ThirdPartyVoter::CREATE,
],
'edit' => [
'template' => '@ChillThirdParty/ThirdParty/update.html.twig',
'role' => ThirdPartyVoter::UPDATE,
],
'view' => [
'template' => '@ChillThirdParty/ThirdParty/view.html.twig',
'role' => ThirdPartyVoter::SHOW,
]
]
]
],
'apis' => [
[
'class' => \Chill\ThirdPartyBundle\Entity\ThirdParty::class,

View File

@@ -22,6 +22,9 @@
namespace Chill\ThirdPartyBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
@@ -31,6 +34,7 @@ use Symfony\Component\Validator\Constraints as Assert;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
/**
* ThirdParty is a party recorded in the database.
@@ -39,14 +43,13 @@ use Symfony\Component\Serializer\Annotation\Groups;
* all users with the right 'CHILL_3PARTY_3PARTY_SEE', 'CHILL_3PARTY_3 to see, select and edit parties for this
* center.
*
* @ORM\Entity
* @ORM\Table(name="chill_3party.third_party")
* @ORM\Entity(repositoryClass="Chill\ThirdPartyBundle\Repository\ThirdPartyRepository")
* @DiscriminatorMap(typeProperty="type", mapping={
* "thirdparty"=ThirdParty::class
* })
* @ORM\HasLifecycleCallbacks()
*/
class ThirdParty
class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
{
/**
* @var int
@@ -54,7 +57,17 @@ class ThirdParty
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
private ?int $id = null;
const KIND_CONTACT = 'contact';
const KIND_COMPANY = 'company';
const KIND_CHILD = 'child';
/**
* @ORM\Column(name="kind", type="string", length="20", options={"default":""})
* @Groups({"write"})
*/
private ?string $kind = "";
/**
* @var string
@@ -62,7 +75,7 @@ class ThirdParty
* @Assert\Length(min="2")
* @Groups({"read", "write"})
*/
private $name;
private ?string $name = "";
/**
* [fr] Raison sociale
@@ -71,7 +84,16 @@ class ThirdParty
* @Assert\Length(min="3")
* @Groups({"read", "write"})
*/
private $nameCompany;
private ?string $nameCompany = "";
/**
* Canonicalized form composed of name, company name and acronym.
*
* This field is read-only, and is generated on database side.
*
* @ORM\Column(name="canonicalized", type="text", options={"default":""})
*/
private ?string $canonicalized = "";
/**
* [fr] Sigle
@@ -80,7 +102,7 @@ class ThirdParty
* @Assert\Length(min="2")
* @Groups({"read", "write"})
*/
private $acronym;
private ?string $acronym = "";
/**
* @var ThirdPartyCategory
@@ -89,18 +111,20 @@ class ThirdParty
* joinColumns={@ORM\JoinColumn(name="thirdparty_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="category_id", referencedColumnName="id")})
*/
private $categories;
private Collection $categories;
/**
* @var array|null
* @ORM\Column(name="types", type="json", nullable=true)
* @Assert\Count(min=1)
*/
private $types;
/**
* Contact Persons: One Institutional ThirdParty has Many Contact Persons
* @ORM\OneToMany(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty", mappedBy="parent")
* @ORM\OneToMany(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty", mappedBy="parent",
* cascade={"persist"}, orphanRemoval=true)
* @var ThirdParty[]|Collection
* @Assert\Valid(traverse=true)
*/
private Collection $children;
@@ -108,23 +132,24 @@ class ThirdParty
* Institutional ThirdParty: Many Contact Persons have One Institutional ThirdParty
* @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id")
* @Groups({"read"})
*/
private ?ThirdParty $parent;
private ?ThirdParty $parent = null;
/**
* @var ThirdPartyCivility
* @ORM\OneToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdPartyCivility")
* @ORM\JoinColumn(name="civility", referencedColumnName="id", nullable=true)
* @var Civility
* @ORM\ManyToOne(targetEntity=Civility::class)
* ORM\JoinColumn(name="civility", referencedColumnName="id", nullable=true)
*/
private $civility;
private ?Civility $civility = null;
/**
* [fr] Qualité
* @var ThirdPartyProfession
* @ORM\OneToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdPartyProfession")
* @ORM\JoinColumn(name="profession", referencedColumnName="id", nullable=true)
* @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdPartyProfession")
* ORM\JoinColumn(name="profession", referencedColumnName="id", nullable=true)
*/
private $profession;
private ?ThirdPartyProfession $profession = null;
/**
* @var string|null
@@ -132,9 +157,10 @@ class ThirdParty
* @Assert\Regex("/^([\+{1}])([0-9\s*]{4,20})$/",
* message="Invalid phone number: it should begin with the international prefix starting with ""+"", hold only digits and be smaller than 20 characters. Ex: +33123456789"
* )
* @PhonenumberConstraint(type="any")
* @Groups({"read", "write"})
*/
private $telephone;
private ?string $telephone = null;
/**
* @var string|null
@@ -142,7 +168,13 @@ class ThirdParty
* @Assert\Email(checkMX=false)
* @Groups({"read", "write"})
*/
private $email;
private ?string $email = null;
/**
* @var bool
* @ORM\Column(name="contact_data_anonymous", type="boolean", options={"default":false})
*/
private bool $contactDataAnonymous = false;
/**
* @var Address|null
@@ -151,27 +183,24 @@ class ThirdParty
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* @Groups({"read", "write"})
*/
private $address;
private ?Address $address = null;
/**
* Soft-delete flag
* @var boolean
* @ORM\Column(name="active", type="boolean", options={"defaut": true})
*/
private $active = true;
private bool $active = true;
/**
* @var string|null
* @ORM\Column(name="comment", type="text", nullable=true)
*/
private $comment;
private ?string $comment = null;
/**
* @var Collection
* @ORM\ManyToMany(targetEntity="\Chill\MainBundle\Entity\Center")
* @ORM\JoinTable(name="chill_3party.party_center")
*/
private $centers;
private Collection $centers;
/**
* @ORM\Column(name="created_at", type="datetime_immutable", nullable=false)
@@ -179,33 +208,21 @@ class ThirdParty
private \DateTimeImmutable $createdAt;
/**
* @ORM\Column(name="updated_at", type="datetime", nullable=true)
* @ORM\Column(name="updated_at", type="datetime_immutable", nullable=true)
*/
private ?\DateTime $updatedAt;
private ?\DateTimeImmutable $updatedAt;
/**
* @var User
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @ORM\JoinColumn(name="updated_by", referencedColumnName="id")
*/
private $updatedBy;
private ?User $updatedBy;
/**
* @ORM\PrePersist()
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @ORM\JoinColumn(name="created_by", referencedColumnName="id")
*/
public function prePersist()
{
$this->createdAt = new \DateTimeImmutable();
}
/**
* @ORM\PreUpdate()
*/
public function preUpdate()
{
$this->updatedAt = new \DateTime();
}
private ?User $createdBy;
/**
@@ -228,6 +245,17 @@ class ThirdParty
return $this->id;
}
public function getKind(): ?string
{
return $this->kind;
}
public function setKind(?string $kind): ThirdParty
{
$this->kind = $kind;
return $this;
}
/**
* Set name.
*
@@ -297,6 +325,17 @@ class ThirdParty
return $this->email;
}
public function isContactDataAnonymous(): bool
{
return $this->contactDataAnonymous;
}
public function setContactDataAnonymous(bool $contactDataAnonymous): ThirdParty
{
$this->contactDataAnonymous = $contactDataAnonymous;
return $this;
}
/**
* Set comment.
*
@@ -329,7 +368,11 @@ class ThirdParty
public function setTypes(array $type = null)
{
// remove all keys from the input data
$this->type = \array_values($type);
$this->types = \array_values($type);
foreach ($this->children as $child) {
$child->setTypes($type);
}
return $this;
}
@@ -344,6 +387,40 @@ class ThirdParty
return $this->types;
}
public function addType(?string $type): self
{
if (NULL === $type) {
return $this;
}
if (!\in_array($type, $this->types ?? [])) {
$this->types[] = $type;
}
foreach ($this->children as $child) {
$child->addType($type);
}
return $this;
}
public function removeType(?string $type): self
{
if (NULL === $type) {
return $this;
}
if (\in_array($type, $this->types ?? [])) {
$this->types = \array_filter($this->types, fn($e) => !\in_array($e, $this->types));
}
foreach ($this->children as $child) {
$child->removeType($type);
}
return $this;
}
/**
* @return bool
*/
@@ -367,6 +444,9 @@ class ThirdParty
public function setActive(bool $active)
{
$this->active = $active;
foreach ($this->children as $child) {
$child->setActive($active);
}
return $this;
}
@@ -414,14 +494,18 @@ class ThirdParty
*/
public function getAddress(): ?Address
{
if ($this->isChild()) {
return $this->getParent()->getAddress();
}
return $this->address;
}
/**
* @param Address $address
* @param Address|null $address
* @return $this
*/
public function setAddress(Address $address)
public function setAddress(?Address $address = null)
{
$this->address = $address;
@@ -448,9 +532,9 @@ class ThirdParty
* @param string $nameCompany
* @return ThirdParty
*/
public function setNameCompany(string $nameCompany): ThirdParty
public function setNameCompany(?string $nameCompany): ThirdParty
{
$this->nameCompany = $nameCompany;
$this->nameCompany = (string) $nameCompany;
return $this;
}
@@ -466,9 +550,9 @@ class ThirdParty
* @param string $acronym
* @return $this
*/
public function setAcronym(string $acronym): ThirdParty
public function setAcronym(?string $acronym = null): ThirdParty
{
$this->acronym = $acronym;
$this->acronym = (string) $acronym;
return $this;
}
@@ -486,7 +570,14 @@ class ThirdParty
*/
public function addCategory(ThirdPartyCategory $category): self
{
$this->categories[] = $category;
if (!$this->categories->contains($category)) {
$this->categories[] = $category;
}
foreach ($this->children as $child) {
$child->addCategory($category);
}
return $this;
}
@@ -497,6 +588,11 @@ class ThirdParty
public function removeCategory(ThirdPartyCategory $category): self
{
$this->categories->removeElement($category);
foreach ($this->children as $child) {
$child->removeCategory($category);
}
return $this;
}
@@ -509,19 +605,18 @@ class ThirdParty
}
/**
* isLeaf aliases
* @Groups({"read"})
*/
public function isChild():bool
{
return $this->isLeaf();
return $this->parent !== null;
}
public function isParent():bool
{
return !$this->isLeaf();
return !$this->isChild();
}
/**
* @return Collection
*/
@@ -530,6 +625,113 @@ class ThirdParty
return $this->children;
}
/**
* Get the children where active = true
*
* @return Collection
*/
public function getActiveChildren(): Collection
{
return $this->children->filter(fn (ThirdParty $tp) => $tp->getActive());
}
/**
* Add a child and set the child as active
*
* Method used in conjonction with getActiveChildren in form.
*
* @internal use the method addChild
* @param ThirdParty $child
* @return $this
*/
public function addActiveChild(ThirdParty $child): self
{
$child->setActive(true);
return $this->addChild($child);
}
/**
* mark the child as unactive, but keep the child existing in the
* database. To effectively remove the child, use removeChild instead.
*
* @param ThirdParty $child
* @return $this
*/
public function removeActiveChild(ThirdParty $child): self
{
$child->setActive(false);
return $this;
}
public function addTypesAndCategories($typeAndCategory): self
{
if ($typeAndCategory instanceof ThirdPartyCategory) {
$this->addCategory($typeAndCategory);
return $this;
}
if (is_string($typeAndCategory)) {
$this->addType($typeAndCategory);
return $this;
}
throw new \UnexpectedValueException(sprintf(
"typeAndCategory should be a string or a %s", ThirdPartyCategory::class));
}
public function removeTypesAndCategories($typeAndCategory): self
{
if ($typeAndCategory instanceof ThirdPartyCategory) {
$this->removeCategory($typeAndCategory);
return $this;
}
if (is_string($typeAndCategory)) {
$this->removeType($typeAndCategory);
return $this;
}
throw new \UnexpectedValueException(sprintf(
"typeAndCategory should be a string or a %s", ThirdPartyCategory::class));
}
public function getTypesAndCategories(): array
{
return \array_merge(
$this->getCategories()->toArray(),
$this->getTypes() ?? []
);
}
public function setTypesAndCategories(array $typesAndCategories): self
{
$types = \array_filter($typesAndCategories, fn($item) => !$item instanceof ThirdPartyCategory);
$this->setTypes($types);
// handle categories
foreach ($typesAndCategories as $t) {
$this->addTypesAndCategories($t);
}
$categories = \array_filter($typesAndCategories, fn($item) => $item instanceof ThirdPartyCategory);
$categoriesHashes = \array_map(fn(ThirdPartyCategory $c) => \spl_object_hash($c), $categories);
foreach ($categories as $c) {
$this->addCategory($c);
}
foreach ($this->getCategories() as $t) {
if (!\in_array(\spl_object_hash($t), $categoriesHashes)) {
$this->removeCategory($t);
}
}
return $this;
}
/**
* @param ThirdParty $child
* @return $this
@@ -537,17 +739,24 @@ class ThirdParty
public function addChild(ThirdParty $child): self
{
$this->children[] = $child;
$child->setParent($this)->setKind(ThirdParty::KIND_CHILD);;
return $this;
}
/**
* Remove the child from the database.
*
* If you want to keep the child into the database
* but desactivate it, use removeActiveChildren instead.
*
* @param ThirdParty $child
* @return $this
*/
public function removeChild(ThirdParty $child): self
{
$this->children->removeElement($child);
$this->active = false;
return $this;
}
@@ -570,18 +779,18 @@ class ThirdParty
}
/**
* @return ThirdPartyCivility|null
* @return Civility|null
*/
public function getCivility(): ?ThirdPartyCivility
public function getCivility(): ?Civility
{
return $this->civility;
}
/**
* @param ThirdPartyCivility $civility
* @param Civility $civility
* @return $this
*/
public function setCivility(ThirdPartyCivility $civility): ThirdParty
public function setCivility(Civility $civility): ThirdParty
{
$this->civility = $civility;
return $this;
@@ -617,7 +826,7 @@ class ThirdParty
* @param \DateTimeImmutable $createdAt
* @return $this
*/
public function setCreatedAt(\DateTimeImmutable $createdAt): ThirdParty
public function setCreatedAt(\DateTimeInterface $createdAt): ThirdParty
{
$this->createdAt = $createdAt;
return $this;
@@ -626,16 +835,16 @@ class ThirdParty
/**
* @return \DateTime|null
*/
public function getUpdatedAt(): ?\DateTime
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
/**
* @param \DateTime $updatedAt
* @param \DateTimeImmutable $updatedAt
* @return $this
*/
public function setUpdatedAt(\DateTime $updatedAt): ThirdParty
public function setUpdatedAt(\DateTimeInterface $updatedAt): ThirdParty
{
$this->updatedAt = $updatedAt;
return $this;
@@ -659,6 +868,12 @@ class ThirdParty
return $this;
}
public function setCreatedBy(User $user): TrackCreationInterface
{
$this->createdBy = $user;
return $this;
}
}

View File

@@ -3,23 +3,30 @@
namespace Chill\ThirdPartyBundle\Form;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\PickAddressType;
use Chill\MainBundle\Form\Type\PickCenterType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCivility;
use Chill\ThirdPartyBundle\Entity\ThirdPartyProfession;
use Chill\ThirdPartyBundle\Form\Type\PickThirdPartyType;
use Chill\ThirdPartyBundle\Form\Type\PickThirdPartyTypeCategoryType;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Chill\ThirdPartyBundle\Security\Voter\ThirdPartyVoter;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Security\Core\Role\Role;
use Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeManager;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -37,14 +44,14 @@ class ThirdPartyType extends AbstractType
protected TranslatableStringHelper $translatableStringHelper;
protected ObjectManager $om;
protected EntityManagerInterface $om;
public function __construct(
AuthorizationHelper $authorizationHelper,
TokenStorageInterface $tokenStorage,
ThirdPartyTypeManager $typesManager,
TranslatableStringHelper $translatableStringHelper,
ObjectManager $om
EntityManagerInterface $om
) {
$this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage;
@@ -58,58 +65,10 @@ class ThirdPartyType extends AbstractType
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$types = [];
foreach ($this->typesManager->getProviders() as $key => $provider) {
$types['chill_3party.key_label.'.$key] = $key;
}
if (count($types) === 1) {
$builder
->add('types', HiddenType::class, [
'data' => array_values($types)
])
->get('types')
->addModelTransformer(new CallbackTransformer(
function (?array $typeArray): ?string {
if (null === $typeArray) {
return null;
}
return implode(',', $typeArray);
},
function (?string $typeStr): ?array {
if (null === $typeStr) {
return null;
}
return explode(',', $typeStr);
}
))
;
} else {
$builder->add('types', ChoiceType::class, [
'choices' => $types,
'expanded' => true,
'multiple' => true,
'label' => 'thirdparty.Type'
]);
}
$builder
->add('name', TextType::class, [
'required' => true
])
->add('categories', EntityType::class, [
'label' => 'thirdparty.Categories',
'class' => ThirdPartyCategory::class,
'choice_label' => function (ThirdPartyCategory $category): string {
return $this->translatableStringHelper->localize($category->getName());
},
'query_builder' => function (EntityRepository $er): QueryBuilder {
return $er->createQueryBuilder('c')
->where('c.active = true');
},
'required' => true,
'multiple' => true,
'attr' => ['class' => 'select2']
])
->add('telephone', TextType::class, [
'label' => 'Phonenumber',
'required' => false
@@ -117,54 +76,26 @@ class ThirdPartyType extends AbstractType
->add('email', EmailType::class, [
'required' => false
])
->add('active', ChoiceType::class, [
'label' => 'thirdparty.Status',
'choices' => [
'Active, shown to users' => true,
'Inactive, not shown to users' => false
],
'expanded' => true,
'multiple' => false
])
->add('comment', ChillTextareaType::class, [
'required' => false
])
->add('centers', EntityType::class, [
'choices' => $this->getReachableCenters($options),
'class' => \Chill\MainBundle\Entity\Center::class,
'multiple' => true,
'attr' => ['class' => 'select2']
->add('centers', PickCenterType::class, [
'role' => (\array_key_exists('data', $options) && $this->om->contains($options['data'])) ?
ThirdPartyVoter::UPDATE : ThirdPartyVoter::CREATE,
'choice_options' => [
'multiple' => true,
'attr' => ['class' => 'select2']
]
])
;
$builder
->add('address', HiddenType::class)
->get('address')
->addModelTransformer(new CallbackTransformer(
function (?Address $address): string {
if (null === $address) {
return '';
}
return $address->getId();
},
function (?string $addressId): ?Address {
if (null === $addressId) {
return null;
}
return $this->om
->getRepository(Address::class)
->findOneBy(['id' => (int) $addressId]);
}
))
;
// Contact Person ThirdParty (child)
if ($options['data']->isChild()) {
if (ThirdParty::KIND_CONTACT === $options['kind'] || ThirdParty::KIND_CHILD === $options['kind']) {
$builder
->add('civility', EntityType::class, [
'label' => 'thirdparty.Civility',
'class' => ThirdPartyCivility::class,
'choice_label' => function (ThirdPartyCivility $civility): string {
'class' => Civility::class,
'choice_label' => function (Civility $civility): string {
return $this->translatableStringHelper->localize($civility->getName());
},
'query_builder' => function (EntityRepository $er): QueryBuilder {
@@ -172,7 +103,7 @@ class ThirdPartyType extends AbstractType
->where('c.active = true');
},
'placeholder' => 'thirdparty.choose civility',
'required' => true
'required' => false
])
->add('profession', EntityType::class, [
'label' => 'thirdparty.Profession',
@@ -187,11 +118,24 @@ class ThirdPartyType extends AbstractType
'placeholder' => 'thirdparty.choose profession',
'required' => false
])
->add('contactDataAnonymous', CheckboxType::class, [
'required' => false,
'label' => 'thirdparty.Contact data are confidential'
])
;
// Institutional ThirdParty (parent)
} else {
$builder
->add('address', PickAddressType::class, [
'label' => 'Address'
])
->add('address2', PickAddressType::class, [
'label' => 'Address',
'use_valid_from' => true,
'use_valid_to' => true,
'mapped' => false,
])
->add('nameCompany', TextType::class, [
'label' => 'thirdparty.NameCompany',
'required' => false
@@ -200,49 +144,51 @@ class ThirdPartyType extends AbstractType
'label' => 'thirdparty.Acronym',
'required' => false
])
->add('activeChildren', ChillCollectionType::class, [
'entry_type' => ThirdPartyType::class,
'entry_options' => [
'is_child' => true,
'block_name' => 'children',
'kind' => ThirdParty::KIND_CHILD,
],
'block_name' => 'active_children',
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'button_add_label' => "Add a contact",
'button_remove_label' => "Remove a contact",
'empty_collection_explain' => "Any contact"
])
;
}
}
/**
*
* @param array $options
* @return \Chill\MainBundle\Entity\Center[]
*/
protected function getReachableCenters(array $options)
{
switch($options['usage']) {
case 'create': $role = new Role(ThirdPartyVoter::CREATE);
break;
case 'update': $role = new Role(ThirdPartyVoter::UPDATE);
break;
if (ThirdParty::KIND_CHILD !== $options['kind']) {
$builder
->add('typesAndCategories', PickThirdPartyTypeCategoryType::class, [
'label' => 'thirdparty.Categories'
])
->add('active', ChoiceType::class, [
'label' => 'thirdparty.Status',
'choices' => [
'Active, shown to users' => true,
'Inactive, not shown to users' => false
],
'expanded' => true,
'multiple' => false
]);
}
return $this->authorizationHelper->getReachableCenters(
$this->tokenStorage->getToken()->getUser(), $role);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Chill\ThirdPartyBundle\Entity\ThirdParty'
'data_class' => ThirdParty::class,
'is_child' => false,
'kind' => null
));
$resolver->setRequired('usage')
->setAllowedValues('usage', ['create', 'update'])
;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'chill_thirdpartybundle_thirdparty';
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Chill\ThirdPartyBundle\Form\Type;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
use Chill\ThirdPartyBundle\Repository\ThirdPartyCategoryRepository;
use Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeManager;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class PickThirdPartyTypeCategoryType extends \Symfony\Component\Form\AbstractType
{
private ThirdPartyCategoryRepository $thirdPartyCategoryRepository;
private ThirdPartyTypeManager $thirdPartyTypeManager;
private TranslatableStringHelper $translatableStringHelper;
private TranslatorInterface $translator;
private const PREFIX_TYPE = 'chill_3party.key_label.';
public function __construct(
ThirdPartyCategoryRepository $thirdPartyCategoryRepository,
ThirdPartyTypeManager $thirdPartyTypeManager,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator
) {
$this->thirdPartyCategoryRepository = $thirdPartyCategoryRepository;
$this->thirdPartyTypeManager = $thirdPartyTypeManager;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$choices = \array_merge(
$this->thirdPartyCategoryRepository->findBy(['active' => true]),
$this->thirdPartyTypeManager->getTypes()
);
\uasort($choices, function ($itemA, $itemB) {
$strA = $itemA instanceof ThirdPartyCategory ? $this->translatableStringHelper
->localize($itemA->getName()) : $this->translator->trans(self::PREFIX_TYPE.$itemA);
$strB = $itemB instanceof ThirdPartyCategory ? $this->translatableStringHelper
->localize($itemB->getName()) : $this->translator->trans(self::PREFIX_TYPE.$itemB);
return $strA <=> $strB;
});
$resolver->setDefaults([
'choices' => $choices,
'attr' => [ 'class' => 'select2' ],
'multiple' => true,
'choice_label' => function($item) {
if ($item instanceof ThirdPartyCategory) {
return $this->translatableStringHelper->localize($item->getName());
}
return self::PREFIX_TYPE.$item;
},
'choice_value' => function($item) {
return $this->reverseTransform($item);
}
]);
}
public function reverseTransform($value)
{
if ($value === null) {
return null;
}
if (is_array($value)){
$r = [];
foreach ($value as $v) {
$r[] = $this->transform($v);
}
return $r;
}
if ($value instanceof ThirdPartyCategory) {
return 'category:'.$value->getId();
}
if (is_string($value)) {
return 'type:'.$value;
}
throw new UnexpectedTypeException($value, \implode(' or ', ['array', 'string', ThirdPartyCategory::class]));
}
}

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