Compare commits

..

125 Commits

Author SHA1 Message Date
2dcc65d172 unresolved! temporary disable deprecated appendScopeChoices function in reportType form 2023-03-29 13:49:10 +02:00
2d6a0d14eb Fix syntax in ReportController 2023-03-29 13:48:24 +02:00
fcb057c55b resolve center in Report Controller 2023-03-29 13:47:34 +02:00
96ddc73e45 Feature: [calendar sync msgraph] Allow to show the calendar details if the user does not have msgraph mapping
We first check that there are "msgraph" attributes before forcing redirection to MSGraph authorization. If not, we let's the user see the calendar edit/create page.
2023-03-23 12:48:37 +01:00
4eb7d10e45 Fixed: [calendar ms synchro] do not throw an error if we are not allowed
to get the default calendar
2023-03-23 11:23:14 +01:00
efa475df0f Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles 2023-03-22 09:06:12 +01:00
a8977729fe Fixed: [similar person matcher] properly takes person center into account
Center comes from the table person_center_history, not person.center_id
2023-03-21 16:39:31 +01:00
ecac409586 fixed: fix 3party syntax 2023-03-20 21:38:42 +01:00
df2480c47c Fixed: transform null value into emtpy string into ThirdParty::setProfession 2023-03-20 21:36:46 +01:00
c729a14304 FIX [minor] remove dump, use statement, ... 2023-03-20 07:54:16 +01:00
06b7e84270 FIX [thirdparty][profession] set default value for profession in symfony form 2023-03-16 13:48:01 +01:00
1cc80c8e6a Fixed: takes all activity into account to check social issue consistency 2023-03-16 00:12:41 +01:00
c3558beee1 Fixed: add a social issue on an activity to an accompanying period 2023-03-16 00:12:09 +01:00
44ecad2bca Fixed: re-introduce creator in async doc generation 2023-03-15 13:38:19 +01:00
d1bdf41c4c Feature: Force language when converting documents 2023-03-15 13:36:41 +01:00
4a30f310b8 FIX [referrer][deselect] allow referrer to be deselected in parcours edit form 2023-03-13 16:41:43 +01:00
896b4cdfe3 FIX [regroupment][datamapper] add condition to check if key exists in forms array 2023-03-10 15:36:18 +01:00
a272dabcaf Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles 2023-03-10 12:04:10 +01:00
3901fe2d32 FIX [translations][dutch] change some dutch translations 2023-03-10 12:03:33 +01:00
78858e84f2 Fixed: force a name when downloading a document without filename
see https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/1005
2023-03-09 18:01:39 +01:00
cc98f64be5 Merge branch 'issue702_document_title_activity_listing' into 'master'
Show document titles in activity listing

See merge request Chill-Projet/chill-bundles!498
2023-03-09 16:49:39 +00:00
6e812b54e1 Fixed: add small button and ACL on document edit 2023-03-09 17:47:52 +01:00
83e0a50b57 Fixed: do not alter layout for bootstrap 2023-03-09 17:47:32 +01:00
3aac4d5d35 FEATURE add download button to document listing 2023-03-09 17:41:31 +01:00
23cee274a5 FEATURE [documents][listing] show document titles in activity listing if there are any and user has see_detail rights 2023-03-09 17:41:31 +01:00
aacb54037b Merge branch '56-user-badges' into 'master'
Fixed: uniformize user badge #56

Closes #56

See merge request Chill-Projet/chill-bundles!503
2023-03-09 16:09:28 +00:00
57cb96320c Merge branch '54-display-action-button' into 'master'
Fixed: graphical bug: the download action button is overflowed #54

See merge request Chill-Projet/chill-bundles!501
2023-03-09 15:54:59 +00:00
5a2d80cb4d Merge branch '721-bug-location' into 'master'
Bug on Location admin page

See merge request Chill-Projet/chill-bundles!505
2023-03-09 14:26:32 +00:00
2e822a9486 Fix bug on admin Location Page with availableForUsers value 2023-03-09 12:35:29 +01:00
f376b1af49 UX: [badges] uniformize user badges in lists, shows and dashboards 2023-03-07 16:23:41 +01:00
dd621186e8 improve 2023-03-06 18:07:06 +01:00
1b15abe635 oops.. with condition 2023-03-06 17:59:55 +01:00
1965fc55f4 Fixed button DocumentActionsButtonsGroup display bug
- improve document table
- add a smallfont css classe to handle table style
when called from index or from show page
2023-03-06 17:47:33 +01:00
a9290eb3fe Merge branch 'issue704_private_comment_listing' into 'master'
Show private comment in activity listing

See merge request Chill-Projet/chill-bundles!497
2023-03-06 12:41:37 +00:00
e78eb8789d FIX [private comment] show private comment in activity listings 2023-03-06 13:39:34 +01:00
9911112e08 Merge branch 'issue711_confirm_referrer' into 'master'
Confirmation popup when referrer is changed for parcours

See merge request Chill-Projet/chill-bundles!496
2023-03-06 12:30:30 +00:00
c0675aee9b Fixed: set referrer from suggestion 2023-03-06 13:28:47 +01:00
03ee04978c FEATURE [confirm][popup] Add a popup asking for confirmation when referrer is changed to prevent mistakes and unecessary notifications 2023-03-06 13:20:10 +01:00
15d68df8c6 Merge branch 'issue716_change_field_thirdparty' into 'master'
Change profession field into string type

See merge request Chill-Projet/chill-bundles!492
2023-03-06 12:12:29 +00:00
1b2c0ecc87 fixed: use TEXT instead of VARCHAR to store profession 2023-03-06 13:01:22 +01:00
f15017ebd7 FIX don't delete profession_id column yet 2023-03-06 13:01:22 +01:00
9a56a1b115 FEATURE [migration] migrate the old profession_id to inserting the corresponding string into new profession column string type 2023-03-06 13:01:22 +01:00
11e7f2179c FIX [obsolete] remove obsolete code since thirdparty entity will be removed 2023-03-06 13:01:22 +01:00
e982e81900 FIX [post] adjust vue components to allow thirdparty to be posted with new profession type as string 2023-03-06 13:01:21 +01:00
e50b02a8c7 FIX [rendering] adjust vue components to correctly render profession string 2023-03-06 13:01:21 +01:00
bea839663f FEATURE [form] adjust symfony form to use text type 2023-03-06 13:01:21 +01:00
ac4c821290 FEATURE [profession] change field type of profession into string 2023-03-06 13:01:20 +01:00
c953da3fd0 Merge branch 'issue715_household_move_email' into 'master'
Issue715 household move email

See merge request Chill-Projet/chill-bundles!495
2023-03-06 11:38:13 +00:00
f5d17eb38c Merge remote-tracking branch 'origin/master' into issue715_household_move_email 2023-03-06 12:20:20 +01:00
73f332927d FIX [personMatcher] wrong syntax was used to setParameter for query in similar person matcher 2023-03-02 11:45:41 +01:00
ef75deda26 FIX [regroupment] remove CenterCompilerPass - no longer in use 2023-03-02 11:23:29 +01:00
6264a95d62 Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles 2023-03-02 11:12:12 +01:00
26a6169b95 FIX [budget][calculator] fix method call getAlias() 2023-03-02 11:11:40 +01:00
678defdee7 FIX [regroupment][form] add check to display regroupment field in the form or not. Only if regroupments exist 2023-03-02 10:52:48 +01:00
88ccbd450a Merge branch 'integrate_regroupment_entity' into 'master'
Feature: ask for center regroupment in exports

See merge request Chill-Projet/chill-bundles!486
2023-03-01 16:13:32 +00:00
62532e0a90 Fixes for feature "Regroupment of center"
* allow more than 15 character in regroupment name
* remove unused methods in PickCenterType
* show only active Regroupment in form
* remove dead code and commented code
2023-03-01 17:11:29 +01:00
73fa585707 Merge remote-tracking branch 'origin/master' into integrate_regroupment_entity 2023-03-01 16:50:53 +01:00
21a16dcbe2 Merge branch 'user_absences' into 'master'
Feature: allow users to say they are out of office

See merge request Chill-Projet/chill-bundles!476
2023-03-01 15:40:22 +00:00
b30e966316 DX: [absence] Remove unneccessary $em->persist() 2023-03-01 16:32:20 +01:00
9696a8194c DX: use the UserRenderBoxBadge to display user in AddPerson 2023-03-01 16:10:51 +01:00
6749758b46 Fixes for features "allow to set user absences" 2023-03-01 16:08:49 +01:00
f1ebc089c3 Merge remote-tracking branch 'origin/master' into user_absences 2023-03-01 14:59:43 +01:00
813adc70f4 Merge branch '52-repair-fixtures' into 'master'
Repair fixtures

See merge request Chill-Projet/chill-bundles!494
2023-03-01 13:57:21 +00:00
95984eff6d Apply 1 suggestion(s) to 1 file(s) 2023-03-01 13:55:35 +00:00
3db5b62d57 Feature: [UI] use download button group in calendar list 2023-02-28 22:37:46 +01:00
cf1cc937ca DX: ignore file in .cache directory (in use with php82 branch) 2023-02-28 22:37:00 +01:00
14df8fe9ad DX: remove implicit creation of a variable 2023-02-28 22:09:07 +01:00
fe4388c884 Feature: [docgen] implements normalization of data on CalendarContext 2023-02-28 22:01:52 +01:00
dbcc425f5f DX: improve performance for counting feature linked to person 2023-02-28 17:22:54 +01:00
77c545344c DX: adapt to php 7.4 2023-02-28 16:44:37 +01:00
c13d672db2 Merge branch '49-decouple-docgen' into 'master'
Resolve "Génération de document: mémoire disponible dépassée"

Closes #49

See merge request Chill-Projet/chill-bundles!493
2023-02-28 15:25:47 +00:00
a16244a3f5 Feature: [docgen] generate documents in an async queue
The documents are now generated in a queue, using symfony messenger. This queue should be configured:

```yaml
# app/config/messenger.yaml
framework:
    messenger:
        # reset services after consuming messages
        # reset_on_message: true

        failure_transport: failed

        transports:
            # https://symfony.com/doc/current/messenger.html#transport-configuration
            async: '%env(MESSENGER_TRANSPORT_DSN)%'
            priority:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
            failed: 'doctrine://default?queue_name=failed'

        routing:
            # ... other messages
            'Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage': priority
```

`StoredObject`s now have additionnal properties:

* status (pending, failure, ready (by default) ), which explain if the document is generated;
* a generationTrialCounter, which is incremented on each generation trial, which prevent each generation more than 5 times;

The generator computation is moved from the `DocGenTemplateController` to a `Generator` (implementing `GeneratorInterface`. 

There are new methods to `Context` which allow to normalize/denormalize context data to/from a messenger's `Message`.
2023-02-28 15:25:47 +00:00
27f13e0dd1 FEATURE [menu][counters] show counter icon for person resources and residential address if there are any 2023-02-24 13:54:44 +01:00
f07ea3259e php cs fixes 2023-02-22 11:54:03 +01:00
1f4438690e FEATURE [email] add a line notifying referrer which user has moved a person or household 2023-02-22 11:36:57 +01:00
744b62184a fix LoadHousehold fixture 2023-02-17 14:48:02 +01:00
5ee0ab5ab8 fix LoadInvite fixture (CalendarBundle) 2023-02-17 13:32:33 +01:00
4370349f10 fix LoadCalendarRange fixture 2023-02-17 13:32:05 +01:00
6254303392 Revert "Feature: [docgen][stored object] update model to store status, template_id and fix defaults"
This reverts commit 55918bcafb.
2023-02-16 14:08:16 +01:00
9676975cd8 Revert "Feature: [docgen][stored object] handler for request generator and required fixes"
This reverts commit 91d21ba939.
2023-02-16 14:08:11 +01:00
7c4bc8f46a Revert commits linked to decoupling of docgen (changes not reviewed)
This reverts commit 55a845fcd6.
2023-02-16 14:07:47 +01:00
55a845fcd6 Sanitize DocGeneratorTemplateController constructor 2023-02-14 23:34:21 +01:00
91d21ba939 Feature: [docgen][stored object] handler for request generator and required fixes 2023-02-14 23:26:00 +01:00
55918bcafb Feature: [docgen][stored object] update model to store status, template_id and fix defaults 2023-02-14 20:57:34 +01:00
bb05ba0f17 Feature: [docgen] create a service to generate a document from a template 2023-02-14 19:35:28 +01:00
eac3471cbb FIX [person][contactinfo] place the contactinfo fields underneath birthdate for easier access 2023-02-10 11:37:32 +01:00
f653f8fd7a FIX [translations][wording] make distinction between parties concernees for an activity and an appointment in the translations 2023-02-10 11:30:26 +01:00
6d2c6fb6e1 FIX [renaming][translation] changing a translation for concerned groups in activities 2023-02-10 10:51:16 +01:00
aea6272c4d Merge improvements for admin's budget bundle 2023-02-09 18:20:33 +01:00
d6df16973a Merge branch '693-filter-acp-by-user-job' into 'master'
693 on doit utiliser le métier du référent, et pas le métier du parcours

See merge request Chill-Projet/chill-bundles!482
2023-02-08 15:59:43 +00:00
80835dd7c3 Fixed: [export][filter by user's job] take into account the job associated to the user instead of the one associated to the course 2023-02-08 15:59:42 +00:00
1285100801 Merge branch 'integrate_regroupment_entity' of gitlab.com:Chill-Projet/chill-bundles into integrate_regroupment_entity 2023-02-07 15:49:08 +01:00
a4e21b7834 FEATURE [datamapper][regroupment] minor fix 2023-02-07 15:42:58 +01:00
1a44a516c2 FEATURE [datamapper][regroupment] make the datamapper work 2023-02-07 15:42:58 +01:00
fb9b9b9226 FEATURE [datamapper][regroupment] moved datamapper to seperate class. Still not working 2023-02-07 15:42:58 +01:00
0ace1c1f6a FEATURE [regroupment][form] integrate in regroupment admin entity into PickCenterType, datamapping not working 2023-02-07 15:42:58 +01:00
7d80507517 FEATURE [regroupment][exports] first commit to implement regroupment entity in exports 2023-02-07 15:42:58 +01:00
b5ec0919e7 FEATURE [datamapper][regroupment] minor fix 2023-02-07 15:41:28 +01:00
068311d071 FEATURE [datamapper][regroupment] make the datamapper work 2023-02-07 15:36:57 +01:00
5756a37178 FEATURE [datamapper][regroupment] moved datamapper to seperate class. Still not working 2023-02-07 10:23:19 +01:00
c83e8ad9a4 FEATURE [regroupment][form] integrate in regroupment admin entity into PickCenterType, datamapping not working 2023-02-03 12:29:38 +01:00
5bfd2aefe6 DX: [html] change deprecated tag 2023-01-30 15:20:13 +01:00
2165e04ec3 Fixed: [template] integration of absence alert box 2023-01-30 15:16:38 +01:00
1a8e21a77f Fixed: [vue] title hover translation for absent pill 2023-01-30 15:01:24 +01:00
50bb8f10cf FEATURE [admin] add absence in user index admin 2023-01-27 12:25:05 +01:00
1c673db628 FIX [phpcsfixer][phpstan] 2023-01-27 12:18:19 +01:00
fa8a2c5cc5 FIX remove some whitespaces or empty lines + add btn in absence message homepage (still commented out) 2023-01-27 12:04:11 +01:00
d46304e229 FEATURE [banner][render] display the A icon within accourse banner if user is absent instead of string 2023-01-27 12:03:12 +01:00
4dd81da1ef FIX [routes][redirect] fix route naming and redirection after delete action 2023-01-27 11:23:29 +01:00
882e72b609 FIX fix condition to show form or not + sticky button for delete 2023-01-27 11:15:59 +01:00
bb7d072cc8 FIX [render] use isAbsent method to render user as absent or not + place homepage msg above searchbar 2023-01-27 11:10:17 +01:00
f76c031ff3 FIX [translations][types] remove redundant translator and change datetime field to date field 2023-01-27 10:51:59 +01:00
86b5f4dfac FIX [template][translations] fixes to template and translations 2023-01-27 10:44:25 +01:00
ded71c5997 Fixed: [migration] fix the required comment for doctrine on new column 2023-01-26 17:11:50 +01:00
5ae4eb1bf7 Merge branch 'master' into user_absences 2023-01-26 17:09:54 +01:00
7f9e045d5d FEATURE [regroupment][exports] first commit to implement regroupment entity in exports 2023-01-26 11:28:13 +01:00
de9d53936f FEATURE [styling][absence] give styling 2023-01-12 19:51:43 +01:00
6c1108b8aa FEATURE: [homepage][absence] display message to let user know they are still marked as absent 2023-01-12 13:50:29 +01:00
5bbe5af124 FEATURE [absence][render] add absence tag to renderbox and renderstring 2023-01-12 13:30:28 +01:00
2c5c815f68 FEATURE [admin][absence] add possibility for admin to set absence of a user 2023-01-12 12:02:52 +01:00
44ef21f940 FEATURE [delete][absence] add functionality to unset absence 2023-01-12 11:51:42 +01:00
68998c9156 FEATURE [translations][absence] add translations 2023-01-12 11:50:06 +01:00
b93b78615b FIX [migration][absence] fix the typing in db for absence datetime immuatable 2023-01-12 11:12:32 +01:00
b2924ede70 FEAUTURE [routing][absence] use routing annotation instead of config file 2023-01-12 11:10:41 +01:00
fb51e44e45 FEATURE [absence][in_progress] add absence property to user, create form, controller, template, migration and menu entry 2023-01-11 17:16:37 +01:00
155 changed files with 2709 additions and 766 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ composer.lock
docs/build/
node_modules/*
.php_cs.cache
.cache/*
###> symfony/framework-bundle ###
/.env.local

View File

@@ -340,11 +340,6 @@ parameters:
count: 1
path: src/Bundle/ChillPersonBundle/Form/Type/PersonPhoneType.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 3
path: src/Bundle/ChillPersonBundle/Search/PersonSearch.php
-
message: "#^Method Chill\\\\PersonBundle\\\\Search\\\\PersonSearch\\:\\:renderResult\\(\\) should return string but return statement is missing\\.$#"
count: 1

View File

@@ -257,12 +257,10 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
/**
* Add a social issue.
*
* Note: the social issue consistency (the fact that only yougest social issues
* Note: the social issue consistency (the fact that only youngest social issues
* are kept) is processed by an entity listener:
*
* @see{\Chill\PersonBundle\AccompanyingPeriod\SocialIssueConsistency\AccompanyingPeriodSocialIssueConsistencyEntityListener}
*
* @return $this
*/
public function addSocialIssue(SocialIssue $socialIssue): self
{
@@ -270,6 +268,10 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
$this->socialIssues[] = $socialIssue;
}
if ($this->getAccompanyingPeriod() !== null) {
$this->getAccompanyingPeriod()->addSocialIssue($socialIssue);
}
return $this;
}
@@ -550,6 +552,10 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
{
$this->accompanyingPeriod = $accompanyingPeriod;
foreach ($this->getSocialIssues() as $issue) {
$this->accompanyingPeriod->addSocialIssue($issue);
}
return $this;
}

View File

@@ -50,7 +50,7 @@ class LocationFilter implements FilterInterface
{
$builder->add('accepted_location', PickUserLocationType::class, [
'multiple' => true,
'label' => 'pick location'
'label' => 'pick location',
]);
}

View File

@@ -88,3 +88,11 @@ div.flex-bloc.concerned-groups {
font-size: 120%;
}
}
/// DOCUMENT LIST IN ACTIVITY ITEM
li.document-list-item {
display: flex;
width: 100%;
justify-content: space-between;
margin-bottom: 0.3rem;
}

View File

@@ -68,7 +68,7 @@
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
<div class="wl-col list">
<p class="wl-item">
{{ activity.user|chill_entity_render_box }}
<span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
</p>
</div>
</div>
@@ -137,19 +137,42 @@
{{ activity.comment|chill_entity_render_box({
'disable_markdown': false,
'limit_lines': 3,
'metadata': false
'metadata': false,
}) }}
</div>
</div>
{% endif %}
{# Only if ACL SEE_DETAILS AND/OR only on template SHOW ??
durationTime
travelTime
comment
documents
attendee
#}
{% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) and activity.privateComment.hasCommentForUser(app.user) %}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Private comment'|trans }}</h3>
</div>
<div class="wl-col list">
<section class="chill-entity entity-comment-embeddable">
<blockquote class="chill-user-quote private-quote">
{{ activity.privateComment.comments[app.user.id]|chill_markdown_to_html }}
</blockquote>
</section>
</div>
</div>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) and activity.documents|length > 0 %}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Documents'|trans }}</h3>
</div>
<div class="wl-col list">
<ul>
{% for d in activity.documents %}
<li class="document-list-item">{{ d.title|chill_print_or_message('document.Any title') }} {{ d|chill_document_button_group(d.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: true}) }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
</div>

View File

@@ -8,11 +8,13 @@
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
{% endblock %}
{% block content %}

View File

@@ -23,11 +23,13 @@
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
{% endblock %}
{% block content %}

View File

@@ -41,7 +41,7 @@
{% if activity.user and t.userVisible %}
<li>
<span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span>
<b>{{ activity.user|chill_entity_render_box}}</b>
<span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
</li>
{% endif %}

View File

@@ -35,7 +35,9 @@
<div class="item-row separator">
<dl class="chill_view_data">
<dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt>
<dd>{{ entity.user|chill_entity_render_box }}</dd>
<dd>
<span class="badge-user">{{ entity.user|chill_entity_render_box }}</span>
</dd>
{%- if entity.scope -%}
<dt class="inline">{{ 'Scope'|trans }}</dt>
@@ -168,7 +170,7 @@
{% if entity.documents|length > 0 %}
<ul>
{% for d in entity.documents %}
<li>{{ d.title }} {{ d|chill_document_button_group() }}</li>
<li class="document-list-item">{{ d.title|chill_print_or_message('document.Any title') }} {{ d|chill_document_button_group(d.title, is_granted('CHILL_ACTIVITY_UPDATE', entity), {small: true}) }}</li>
{% endfor %}
</ul>
{% else %}

View File

@@ -22,6 +22,7 @@ use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
@@ -45,6 +46,8 @@ class ActivityContext implements
private PersonRenderInterface $personRender;
private PersonRepository $personRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator;
@@ -55,6 +58,7 @@ class ActivityContext implements
TranslatableStringHelperInterface $translatableStringHelper,
EntityManagerInterface $em,
PersonRenderInterface $personRender,
PersonRepository $personRepository,
TranslatorInterface $translator,
BaseContextData $baseContextData
) {
@@ -63,6 +67,7 @@ class ActivityContext implements
$this->translatableStringHelper = $translatableStringHelper;
$this->em = $em;
$this->personRender = $personRender;
$this->personRepository = $personRepository;
$this->translator = $translator;
$this->baseContextData = $baseContextData;
}
@@ -147,7 +152,7 @@ class ActivityContext implements
$options = $template->getOptions();
$data = [];
$data = array_merge($data, $this->baseContextData->getData());
$data = array_merge($data, $this->baseContextData->getData($contextGenerationData['creator'] ?? null));
$data['activity'] = $this->normalizer->normalize($entity, 'docgen', ['docgen:expects' => Activity::class, 'groups' => 'docgen:read']);
$data['course'] = $this->normalizer->normalize($entity->getAccompanyingPeriod(), 'docgen', ['docgen:expects' => AccompanyingPeriod::class, 'groups' => 'docgen:read']);
@@ -206,6 +211,32 @@ class ActivityContext implements
return $options['mainPerson'] || $options['person1'] || $options['person2'];
}
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
$normalized = [];
foreach (['mainPerson', 'person1', 'person2'] as $k) {
$normalized[$k] = null === $data[$k] ? null : $data[$k]->getId();
}
return $normalized;
}
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
$denormalized = [];
foreach (['mainPerson', 'person1', 'person2'] as $k) {
if (null !== ($id = ($data[$k] ?? null))) {
$denormalized[$k] = $this->personRepository->find($id);
} else {
$denormalized[$k] = null;
}
}
return $denormalized;
}
/**
* @param Activity $entity
*/

View File

@@ -146,6 +146,16 @@ class ListActivitiesByAccompanyingPeriodContext implements
return $this->accompanyingPeriodContext->hasPublicForm($template, $entity);
}
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
return $this->accompanyingPeriodContext->contextGenerationDataNormalize($template, $entity, $data);
}
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
return $this->accompanyingPeriodContext->contextGenerationDataDenormalize($template, $entity, $data);
}
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
{
$this->accompanyingPeriodContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData);

View File

@@ -77,7 +77,7 @@ Choose a type: Choisir un type
4 hours: 4 heures
4 hours 30: 4 heures 30
5 hours: 5 heures
Concerned groups: Parties concernées
Concerned groups: Parties concernées par l'échange
Persons in accompanying course: Usagers du parcours
Third persons: Tiers non-pro.
Others persons: Usagers

View File

@@ -1,10 +1,18 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\AsideActivityBundle\Export\Export;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\AsideActivityBundle\Form\AsideActivityCategoryType;
use Chill\AsideActivityBundle\Repository\AsideActivityCategoryRepository;
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
use Chill\AsideActivityBundle\Templating\Entity\CategoryRender;
@@ -16,32 +24,32 @@ use Chill\MainBundle\Export\Helper\UserHelper;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Closure;
use DateTimeInterface;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
final class ListAsideActivity implements ListInterface, GroupedExportInterface
{
private EntityManagerInterface $em;
private UserHelper $userHelper;
private DateTimeHelper $dateTimeHelper;
private ScopeRepositoryInterface $scopeRepository;
private CenterRepositoryInterface $centerRepository;
private AsideActivityCategoryRepository $asideActivityCategoryRepository;
private CategoryRender $categoryRender;
private CenterRepositoryInterface $centerRepository;
private DateTimeHelper $dateTimeHelper;
private EntityManagerInterface $em;
private ScopeRepositoryInterface $scopeRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
private UserHelper $userHelper;
public function __construct(
EntityManagerInterface $em,
DateTimeHelper $dateTimeHelper,
@@ -76,11 +84,6 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
return 'export.aside_activity.List of aside activities';
}
public function getTitle()
{
return 'export.aside_activity.List of aside activities';
}
public function getGroup(): string
{
return 'export.Exports of aside activities';
@@ -91,15 +94,16 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
switch ($key) {
case 'id':
case 'note':
return function ($value) use ($key) {
return static function ($value) use ($key) {
if ('_header' === $value) {
return 'export.aside_activity.' . $key;
}
return $value ?? '';
};
case 'duration':
return function ($value) use ($key) {
return static function ($value) use ($key) {
if ('_header' === $value) {
return 'export.aside_activity.' . $key;
}
@@ -108,7 +112,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
return '';
}
if ($value instanceof \DateTimeInterface) {
if ($value instanceof DateTimeInterface) {
return $value->format('H:i:s');
}
@@ -118,7 +122,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
case 'createdAt':
case 'updatedAt':
case 'date':
return $this->dateTimeHelper->getLabel('export.aside_activity.'.$key);
return $this->dateTimeHelper->getLabel('export.aside_activity.' . $key);
case 'agent_id':
case 'creator_id':
@@ -165,7 +169,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
};
default:
throw new \LogicException('this key is not supported : ' . $key);
throw new LogicException('this key is not supported : ' . $key);
}
}
@@ -182,7 +186,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
'aside_activity_type',
'date',
'duration',
'note'
'note',
];
}
@@ -195,6 +199,11 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
return $query->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
}
public function getTitle()
{
return 'export.aside_activity.List of aside activities';
}
public function getType(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
@@ -204,8 +213,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
{
$qb = $this->em->createQueryBuilder()
->from(AsideActivity::class, 'aside')
->leftJoin('aside.agent', 'agent')
;
->leftJoin('aside.agent', 'agent');
$qb
->addSelect('aside.id AS id')
@@ -218,8 +226,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
->addSelect('IDENTITY(aside.type) AS aside_activity_type')
->addSelect('aside.date')
->addSelect('aside.duration')
->addSelect('aside.note')
;
->addSelect('aside.note');
return $qb;
}

View File

@@ -17,7 +17,6 @@ use Chill\MainBundle\Form\Type\Export\FilterType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;

View File

@@ -15,7 +15,6 @@ use Chill\AsideActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\Entity\UserRender;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;

View File

@@ -29,10 +29,10 @@ class CalculatorManager
public function addCalculator(CalculatorInterface $calculator, bool $default)
{
$this->calculators[$calculator::getAlias()] = $calculator;
$this->calculators[$calculator->getAlias()] = $calculator;
if ($default) {
$this->defaultCalculator[] = $calculator::getAlias();
$this->defaultCalculator[] = $calculator->getAlias();
}
}
@@ -50,7 +50,7 @@ class CalculatorManager
$result = $calculator->calculate($elements);
if (null !== $result) {
$results[$calculator::getAlias()] = $result;
$results[$calculator->getAlias()] = $result;
}
}

View File

@@ -30,7 +30,7 @@ class ChargeKindType extends AbstractType
])
->add('kind', TextType::class, [
'label' => 'budget.admin.form.Charge_kind_key',
'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document'
'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document',
])
->add('ordering', NumberType::class)
->add('isActive', CheckboxType::class, [

View File

@@ -30,7 +30,7 @@ class ResourceKindType extends AbstractType
])
->add('kind', TextType::class, [
'label' => 'budget.admin.form.Resource_kind_key',
'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document'
'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document',
])
->add('ordering', NumberType::class)
->add('isActive', CheckboxType::class, [

View File

@@ -49,8 +49,7 @@ final class ChargeKindRepository implements ChargeKindRepositoryInterface
->where($qb->expr()->eq('c.isActive', 'true'))
->orderBy('c.ordering', 'ASC')
->getQuery()
->getResult()
;
->getResult();
}
/**

View File

@@ -28,8 +28,6 @@ interface ChargeKindRepositoryInterface extends ObjectRepository
*/
public function findAllActive(): array;
public function findOneByKind(string $kind): ?ChargeKind;
/**
* @return ChargeType[]
*/
@@ -45,5 +43,7 @@ interface ChargeKindRepositoryInterface extends ObjectRepository
public function findOneBy(array $criteria): ?ChargeKind;
public function findOneByKind(string $kind): ?ChargeKind;
public function getClassName(): string;
}

View File

@@ -49,8 +49,7 @@ final class ResourceKindRepository implements ResourceKindRepositoryInterface
->where($qb->expr()->eq('r.isActive', 'true'))
->orderBy('r.ordering', 'ASC')
->getQuery()
->getResult()
;
->getResult();
}
/**

View File

@@ -34,7 +34,7 @@ class ResourceRepository extends EntityRepository
//->andWhere('c.startDate < :date')
// TODO: there is a misconception here, the end date must be lower or null. startDate are never null
//->andWhere('c.startDate < :date OR c.startDate IS NULL');
;
;
if (null !== $sort) {
$qb->orderBy($sort);

View File

@@ -13,9 +13,7 @@ namespace Chill\BudgetBundle\Service\Summary;
use Chill\BudgetBundle\Entity\ChargeKind;
use Chill\BudgetBundle\Entity\ResourceKind;
use Chill\BudgetBundle\Repository\ChargeKindRepository;
use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface;
use Chill\BudgetBundle\Repository\ResourceKindRepository;
use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Household\Household;

View File

@@ -20,12 +20,15 @@ use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use ReflectionClass;
use RuntimeException;
/**
* @internal
@@ -47,10 +50,9 @@ final class SummaryBudgetTest extends TestCase
],
]);
$queryCharges->setParameters(Argument::type('array'))
->will(function ($args, $query) {
->will(static function ($args, $query) {
return $query;
})
;
});
$queryResources = $this->prophesize(AbstractQuery::class);
$queryResources->getResult()->willReturn([
@@ -61,23 +63,23 @@ final class SummaryBudgetTest extends TestCase
],
]);
$queryResources->setParameters(Argument::type('array'))
->will(function ($args, $query) {
->will(static function ($args, $query) {
return $query;
})
;
});
$em = $this->prophesize(EntityManagerInterface::class);
$em->createNativeQuery(Argument::type('string'), Argument::type(Query\ResultSetMapping::class))
->will(function ($args) use ($queryResources, $queryCharges) {
->will(static function ($args) use ($queryResources, $queryCharges) {
if (false !== strpos($args[0], 'chill_budget.resource')) {
return $queryResources->reveal();
}
if (false !== strpos($args[0], 'chill_budget.charge')) {
return $queryCharges->reveal();
}
throw new \RuntimeException('this query does not have a stub counterpart: '.$args[0]);
})
;
throw new RuntimeException('this query does not have a stub counterpart: ' . $args[0]);
});
$chargeRepository = $this->prophesize(ChargeKindRepositoryInterface::class);
$chargeRepository->findAll()->willReturn([
@@ -98,24 +100,23 @@ final class SummaryBudgetTest extends TestCase
$resourceRepository->findOneByKind('misc')->willReturn($misc);
$translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class);
$translatableStringHelper->localize(Argument::type('array'))->will(function ($arg) {
$translatableStringHelper->localize(Argument::type('array'))->will(static function ($arg) {
return $arg[0]['fr'];
});
$person = new Person();
$personReflection = new \ReflectionClass($person);
$personReflection = new ReflectionClass($person);
$personIdReflection = $personReflection->getProperty('id');
$personIdReflection->setAccessible(true);
$personIdReflection->setValue($person, 1);
$household = new Household();
$householdReflection = new \ReflectionClass($household);
$householdReflection = new ReflectionClass($household);
$householdId = $householdReflection->getProperty('id');
$householdId->setAccessible(true);
$householdId->setValue($household, 1);
$householdMember = (new HouseholdMember())->setPerson($person)
->setStartDate(new \DateTimeImmutable('1 month ago'))
;
->setStartDate(new DateTimeImmutable('1 month ago'));
$household->addMember($householdMember);
$summaryBudget = new SummaryBudget(

View File

@@ -2,6 +2,13 @@
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Budget;
use Doctrine\DBAL\Schema\Schema;
@@ -9,6 +16,12 @@ use Doctrine\Migrations\AbstractMigration;
final class Version20230209161546 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX resource_kind_unique_type_idx');
$this->addSql('DROP INDEX charge_kind_unique_type_idx');
}
public function getDescription(): string
{
return 'Budget: add unique constraint on kind for charge_kind and resource_kind';
@@ -21,10 +34,4 @@ final class Version20230209161546 extends AbstractMigration
$this->addSql('CREATE UNIQUE INDEX resource_kind_unique_type_idx ON chill_budget.resource_type (kind);');
$this->addSql('CREATE UNIQUE INDEX charge_kind_unique_type_idx ON chill_budget.charge_type (kind);');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX resource_kind_unique_type_idx');
$this->addSql('DROP INDEX charge_kind_unique_type_idx');
}
}

View File

@@ -2,8 +2,8 @@ Budget: Budget
Resource: Inkomsten
Charge: Onkosten
Budget for %name%: Budget van %name%
Budget for household %household%: Budget van gezin
Current budget household members: Actuele budget van gezinsleden
Budget for household %household%: Budget van huishouden
Current budget household members: Actuele budget van leden huishouden
Show budget of %name%: Toon budget van %name%
See complete budget: Toon volledige budget
Hide budget: Verbergen

View File

@@ -61,7 +61,7 @@ class LoadCalendarRange extends Fixture implements FixtureGroupInterface, Ordere
->setEmail('centreA@test.chill.social')
->setLocationType($type = new LocationType())
->setPhonenumber1(PhoneNumberUtil::getInstance()->parse('+3287653812'));
$type->setTitle('Service');
$type->setTitle(['fr' => 'Service']);
$address->setStreet('Rue des Épaules')->setStreetNumber('14')
->setPostcode($postCode = new PostalCode());
$postCode->setCode('4145')->setName('Houte-Si-Plout')->setCountry(

View File

@@ -12,6 +12,8 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\DataFixtures\ORM;
use Chill\CalendarBundle\Entity\Invite;
use Chill\MainBundle\DataFixtures\ORM\LoadUsers;
use Chill\MainBundle\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
@@ -33,14 +35,21 @@ class LoadInvite extends Fixture implements FixtureGroupInterface
public function load(ObjectManager $manager): void
{
$arr = [
['name' => ['fr' => 'Rendez-vous décliné']],
['name' => ['fr' => 'Rendez-vous accepté']],
[
'name' => ['fr' => 'Rendez-vous décliné'],
'status' => Invite::DECLINED,
],
[
'name' => ['fr' => 'Rendez-vous accepté'],
'status' => Invite::ACCEPTED,
],
];
foreach ($arr as $a) {
echo 'Creating calendar invite : ' . $a['name']['fr'] . "\n";
$invite = (new Invite())
->setStatus($a['name']);
->setStatus($a['status'])
->setUser($this->getRandomUser());
$manager->persist($invite);
$reference = 'Invite_' . $a['name']['fr'];
$this->addReference($reference, $invite);
@@ -49,4 +58,11 @@ class LoadInvite extends Fixture implements FixtureGroupInterface
$manager->flush();
}
private function getRandomUser(): User
{
$userRef = array_rand(LoadUsers::$refs);
return $this->getReference($userRef);
}
}

View File

@@ -22,6 +22,7 @@ use Chill\MainBundle\Entity\User;
use DateTimeImmutable;
use LogicException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use function array_key_exists;
@@ -74,9 +75,18 @@ class MapCalendarToUser
public function getDefaultUserCalendar(string $idOrUserPrincipalName): ?array
{
$value = $this->machineHttpClient->request('GET', "users/{$idOrUserPrincipalName}/calendars", [
'query' => ['$filter' => 'isDefaultCalendar eq true'],
])->toArray()['value'];
try {
$value = $this->machineHttpClient->request('GET', "users/{$idOrUserPrincipalName}/calendars", [
'query' => ['$filter' => 'isDefaultCalendar eq true'],
])->toArray()['value'];
} catch (ClientExceptionInterface $e) {
$this->logger->error('[MapCalendarToUser] Error while listing calendars for a user', [
'http_status_code' => $e->getResponse()->getStatusCode(),
'id_user' => $idOrUserPrincipalName,
]);
return null;
}
return $value[0] ?? null;
}

View File

@@ -34,6 +34,7 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -64,6 +65,8 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
private OnBehalfOfUserHttpClient $userHttpClient;
private Security $security;
public function __construct(
CalendarRepository $calendarRepository,
CalendarRangeRepository $calendarRangeRepository,
@@ -74,7 +77,8 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
OnBehalfOfUserHttpClient $userHttpClient,
RemoteEventConverter $remoteEventConverter,
TranslatorInterface $translator,
UrlGeneratorInterface $urlGenerator
UrlGeneratorInterface $urlGenerator,
Security $security
) {
$this->calendarRepository = $calendarRepository;
$this->calendarRangeRepository = $calendarRangeRepository;
@@ -86,6 +90,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
$this->translator = $translator;
$this->urlGenerator = $urlGenerator;
$this->userHttpClient = $userHttpClient;
$this->security = $security;
}
public function countEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): int
@@ -133,6 +138,24 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
public function isReady(): bool
{
$user = $this->security->getUser();
if (!$user instanceof User) {
// this is not a user from chill. This is not the role of this class to
// restrict access, so we will just say that we do not have to do anything more
// here...
return true;
}
if (null === $this->mapCalendarToUser->getUserId($user)) {
// this user is not mapped with remote calendar. The user will have to wait for
// the next calendar subscription iteration
$this->logger->debug('mark user ready for msgraph calendar as he does not have any mapping', [
'userId' => $user->getId(),
]);
return true;
}
return $this->tokenStorage->hasToken();
}

View File

@@ -17,30 +17,20 @@
<td class="eval">
<ul class="eval_title">
<li>
{{ mm.mimeIcon(d.storedObject.type) }}
{{ d.storedObject.title }}
{% if d.dateTimeVersion < d.calendar.dateTimeVersion %}
<span class="badge bg-danger">{{ 'chill_calendar.Document outdated'|trans }}</span>
{% endif %}
<ul class="record_actions small inline">
{% if chill_document_is_editable(d.storedObject) and is_granted('CHILL_CALENDAR_DOC_EDIT', d) %}
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendardoc_delete', {'id': d.id})}}" class="btn btn-delete"></a>
</li>
<li>
{{ d.storedObject|chill_document_edit_button }}
</li>
{% endif %}
{% if is_granted('CHILL_CALENDAR_DOC_EDIT', d) %}
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendardoc_edit', {'id': d.id})}}" class="btn btn-edit"></a>
</li>
{% endif %}
<li>
{{ m.download_button(d.storedObject, d.storedObject.title) }}
</li>
</ul>
<div class="row">
<div class="col text-start">
{{ d.storedObject.title }}
{% if d.dateTimeVersion < d.calendar.dateTimeVersion %}
<span class="badge bg-danger">{{ 'chill_calendar.Document outdated'|trans }}</span>
{% endif %}
</div>
<div class="col-md-auto text-center">
{{ mm.mimeIcon(d.storedObject.type) }}
</div>
<div class="col col-lg-4 text-end">
{{ d.storedObject|chill_document_button_group(d.storedObject.title, is_granted('CHILL_CALENDAR_DOC_EDIT', d), {'small': true}) }}
</div>
</div>
</li>
</ul>
</td>

View File

@@ -7,7 +7,7 @@
<div id="mainUser"></div> {# <=== vue component: mainUser #}
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
<h2 class="chill-red">{{ 'Concerned groups calendar'|trans }}</h2>
{%- if form.persons is defined -%}
{{ form_widget(form.persons) }}

View File

@@ -10,13 +10,13 @@
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_answer') }}
{{ encore_entry_script_tags('mod_async_upload') }}
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_answer') }}
{{ encore_entry_link_tags('mod_async_upload') }}
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
{% endblock %}
{% block content %}

View File

@@ -7,7 +7,7 @@
<div id="mainUser"></div> {# <=== vue component: mainUser #}
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
<h2 class="chill-red">{{ 'Concerned groups calendar'|trans }}</h2>
{%- if form.mainUser is defined -%}
{{ form_row(form.mainUser) }}

View File

@@ -14,7 +14,7 @@
<dd>{{ entity.mainUser }}</dd>
</dl>
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
<h2 class="chill-red">{{ 'Concerned groups calendar'|trans }}</h2>
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': 'calendar_' ~ context, 'render': 'bloc' } %}

View File

@@ -18,8 +18,10 @@ use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
@@ -39,6 +41,10 @@ final class CalendarContext implements CalendarContextInterface
private PersonRender $personRender;
private PersonRepository $personRepository;
private ThirdPartyRepository $thirdPartyRepository;
private ThirdPartyRender $thirdPartyRender;
private TranslatableStringHelperInterface $translatableStringHelper;
@@ -48,14 +54,18 @@ final class CalendarContext implements CalendarContextInterface
EntityManagerInterface $entityManager,
NormalizerInterface $normalizer,
PersonRender $personRender,
PersonRepository $personRepository,
ThirdPartyRender $thirdPartyRender,
ThirdPartyRepository $thirdPartyRepository,
TranslatableStringHelperInterface $translatableStringHelper
) {
$this->baseContextData = $baseContextData;
$this->entityManager = $entityManager;
$this->normalizer = $normalizer;
$this->personRender = $personRender;
$this->personRepository = $personRepository;
$this->thirdPartyRender = $thirdPartyRender;
$this->thirdPartyRepository = $thirdPartyRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
@@ -146,7 +156,7 @@ final class CalendarContext implements CalendarContextInterface
$options = $this->getOptions($template);
$data = array_merge(
$this->baseContextData->getData(),
$this->baseContextData->getData($contextGenerationData['creator'] ?? null),
[
'calendar' => $this->normalizer->normalize($entity, 'docgen', ['docgen:expects' => Calendar::class, 'groups' => ['docgen:read']]),
]
@@ -226,8 +236,44 @@ final class CalendarContext implements CalendarContextInterface
return true;
}
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
$normalized = [];
$normalized['title'] = $data['title'] ?? '';
foreach (['mainPerson', 'thirdParty'] as $k) {
if (isset($data[$k])) {
$normalized[$k] = $data[$k]->getId();
}
}
return $normalized;
}
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
$denormalized = [];
$denormalized['title'] = $data['title'];
if (null !== ($data['mainPerson'] ?? null)) {
if (null === $person = $this->personRepository->find($data['mainPerson'])) {
throw new \RuntimeException('person not found');
}
$denormalized['mainPerson'] = $person;
}
if (null !== ($data['thirdParty'] ?? null)) {
if (null === $thirdParty = $this->thirdPartyRepository->find($data['thirdParty'])) {
throw new \RuntimeException('third party not found');
}
$denormalized['thirdParty'] = $thirdParty;
}
return $denormalized;
}
/**
* @param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData
* param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData
*/
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
{

View File

@@ -56,6 +56,10 @@ interface CalendarContextInterface extends DocGeneratorContextWithPublicFormInte
*/
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array;
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array;
/**
* @param Calendar $entity
*/

View File

@@ -205,7 +205,7 @@ final class CalendarContextTest extends TestCase
?NormalizerInterface $normalizer = null
): CalendarContext {
$baseContext = $this->prophesize(BaseContextData::class);
$baseContext->getData()->willReturn(['base_context' => 'data']);
$baseContext->getData(null)->willReturn(['base_context' => 'data']);
$personRender = $this->prophesize(PersonRender::class);
$personRender->renderString(Argument::type(Person::class), [])->willReturn('person name');

View File

@@ -4,7 +4,7 @@ My calendar list: Mes rendez-vous
There is no calendar items.: Il n'y a pas de rendez-vous
Remove calendar item: Supprimer le rendez-vous
Are you sure you want to remove the calendar item?: Êtes-vous sûr de vouloir supprimer le rendez-vous?
Concerned groups: Parties concernées
Concerned groups calendar: Parties concernées
Calendar data: Données du rendez-vous
Update calendar: Modifier le rendez-vous
main user concerned: Utilisateur concerné

View File

@@ -23,6 +23,9 @@ interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContext
*/
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void;
/**
* Fill the form with initial data
*/
public function getFormData(DocGeneratorTemplate $template, $entity): array;
/**
@@ -31,4 +34,14 @@ interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContext
* @param mixed $entity
*/
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
/**
* Transform the data from the form into serializable data, storable into messenger's message
*/
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array;
/**
* Reverse the data from the messenger's message into data usable for doc's generation
*/
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array;
}

View File

@@ -16,67 +16,58 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Serializer\Model\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
// TODO à mettre dans services
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;
use function strlen;
use const JSON_PRETTY_PRINT;
final class DocGeneratorTemplateController extends AbstractController
{
private HttpClientInterface $client;
private ContextManager $contextManager;
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
private DriverInterface $driver;
private EntityManagerInterface $entityManager;
private LoggerInterface $logger;
private GeneratorInterface $generator;
private MessageBusInterface $messageBus;
private PaginatorFactory $paginatorFactory;
private StoredObjectManagerInterface $storedObjectManager;
public function __construct(
ContextManager $contextManager,
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
DriverInterface $driver,
LoggerInterface $logger,
GeneratorInterface $generator,
MessageBusInterface $messageBus,
PaginatorFactory $paginatorFactory,
HttpClientInterface $client,
StoredObjectManagerInterface $storedObjectManager,
EntityManagerInterface $entityManager
) {
$this->contextManager = $contextManager;
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->driver = $driver;
$this->logger = $logger;
$this->generator = $generator;
$this->messageBus = $messageBus;
$this->paginatorFactory = $paginatorFactory;
$this->client = $client;
$this->storedObjectManager = $storedObjectManager;
$this->entityManager = $entityManager;
}
@@ -94,7 +85,6 @@ final class DocGeneratorTemplateController extends AbstractController
): Response {
return $this->generateDocFromTemplate(
$template,
$entityClassName,
$entityId,
$request,
true
@@ -115,7 +105,6 @@ final class DocGeneratorTemplateController extends AbstractController
): Response {
return $this->generateDocFromTemplate(
$template,
$entityClassName,
$entityId,
$request,
false
@@ -185,7 +174,6 @@ final class DocGeneratorTemplateController extends AbstractController
private function generateDocFromTemplate(
DocGeneratorTemplate $template,
string $entityClassName,
int $entityId,
Request $request,
bool $isTest
@@ -206,7 +194,7 @@ final class DocGeneratorTemplateController extends AbstractController
if (null === $entity) {
throw new NotFoundHttpException(
sprintf('Entity with classname %s and id %s is not found', $entityClassName, $entityId)
sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId)
);
}
@@ -259,99 +247,68 @@ final class DocGeneratorTemplateController extends AbstractController
}
}
$document = $template->getFile();
if ($isTest && ($contextGenerationData['test_file'] instanceof File)) {
$dataDecrypted = file_get_contents($contextGenerationData['test_file']->getPathname());
} else {
try {
$dataDecrypted = $this->storedObjectManager->read($document);
} catch (Throwable $exception) {
throw $exception;
}
}
// transform context generation data
$contextGenerationDataSanitized =
$context instanceof DocGeneratorContextWithPublicFormInterface ?
$context->contextGenerationDataNormalize($template, $entity, $contextGenerationData)
: [];
// if is test, render the data or generate the doc
if ($isTest && isset($form) && $form['show_data']->getData()) {
return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [
'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT)
'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT),
]);
}
try {
$generatedResource = $this
->driver
->generateFromString(
$dataDecrypted,
$template->getFile()->getType(),
$context->getData($template, $entity, $contextGenerationData),
$template->getFile()->getFilename()
);
} catch (TemplateException $e) {
return new Response(
implode("\n", $e->getErrors()),
400,
[
'Content-Type' => 'text/plain',
]
} elseif ($isTest) {
$generated = $this->generator->generateDocFromTemplate(
$template,
$entityId,
$contextGenerationDataSanitized,
null,
true,
isset($form) ? $form['test_file']->getData() : null
);
}
if ($isTest) {
return new Response(
$generatedResource,
$generated,
Response::HTTP_OK,
[
'Content-Transfer-Encoding', 'binary',
'Content-Type' => 'application/vnd.oasis.opendocument.text',
'Content-Disposition' => 'attachment; filename="generated.odt"',
'Content-Length' => strlen($generatedResource),
'Content-Length' => strlen($generated),
],
);
}
/** @var StoredObject $storedObject */
$storedObject = (new ObjectNormalizer())
->denormalize(
[
'type' => $template->getFile()->getType(),
'filename' => sprintf('%s_odt', uniqid('doc_', true)),
],
StoredObject::class
);
try {
$this->storedObjectManager->write($storedObject, $generatedResource);
} catch (Throwable $exception) {
throw $exception;
}
// this is not a test
// we prepare the object to store the document
$storedObject = (new StoredObject())
->setStatus(StoredObject::STATUS_PENDING)
;
$this->entityManager->persist($storedObject);
try {
$context
->storeGenerated(
$template,
$storedObject,
$entity,
$contextGenerationData
);
} catch (Exception $e) {
$this
->logger
->error(
'Unable to store the associated document to entity',
[
'entityClassName' => $entityClassName,
'entityId' => $entityId,
'contextKey' => $context->getName(),
]
);
throw $e;
}
// we store the generated document
$context
->storeGenerated(
$template,
$storedObject,
$entity,
$contextGenerationData
);
$this->entityManager->flush();
$this->messageBus->dispatch(
new RequestGenerationMessage(
$this->getUser(),
$template,
$entityId,
$storedObject,
$contextGenerationDataSanitized,
)
);
return $this
->redirectToRoute(
'chill_wopi_file_edit',

View File

@@ -53,6 +53,7 @@ final class RelatorioDriver implements DriverInterface
$response = $this->client->request('POST', $this->url, [
'headers' => $form->getPreparedHeaders()->toArray(),
'body' => $form->bodyToIterable(),
'timeout' => '300',
]);
return $response->getContent();

View File

@@ -0,0 +1,16 @@
{{ creator.label }},
{{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }}
{{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }}
{{ 'docgen.failure_email.References'|trans }}:
{% if errors|length > 0 %}
{{ 'docgen.failure_email.The following errors were encoutered'|trans }}:
{% for error in errors %}
- {{ error }}
{% endfor %}
{% endif %}
- template_id: {{ template.id }}
- stored_object_destination_id: {{ stored_object_id }}

View File

@@ -21,18 +21,14 @@ class BaseContextData
{
private NormalizerInterface $normalizer;
private Security $security;
public function __construct(Security $security, NormalizerInterface $normalizer)
public function __construct(NormalizerInterface $normalizer)
{
$this->security = $security;
$this->normalizer = $normalizer;
}
public function getData(): array
public function getData(?User $user = null): array
{
$data = [];
$user = $this->security->getUser();
$data['creator'] = $this->normalizer->normalize(
$user instanceof User ? $user : null,

View File

@@ -0,0 +1,146 @@
<?php
namespace Chill\DocGeneratorBundle\Service\Generator;
use Chill\DocGeneratorBundle\Context\ContextManagerInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\File\File;
class Generator implements GeneratorInterface
{
private ContextManagerInterface $contextManager;
private DriverInterface $driver;
private EntityManagerInterface $entityManager;
private LoggerInterface $logger;
private StoredObjectManagerInterface $storedObjectManager;
private const LOG_PREFIX = '[docgen generator] ';
public function __construct(
ContextManagerInterface $contextManager,
DriverInterface $driver,
EntityManagerInterface $entityManager,
LoggerInterface $logger,
StoredObjectManagerInterface $storedObjectManager
) {
$this->contextManager = $contextManager;
$this->driver = $driver;
$this->entityManager = $entityManager;
$this->logger = $logger;
$this->storedObjectManager = $storedObjectManager;
}
/**
* @template T of File|null
* @template B of bool
* @param B $isTest
* @param (B is true ? T : null) $testFile
* @psalm-return (B is true ? string : null)
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
*/
public function generateDocFromTemplate(
DocGeneratorTemplate $template,
int $entityId,
array $contextGenerationDataNormalized,
?StoredObject $destinationStoredObject = null,
bool $isTest = false,
?File $testFile = null,
?User $creator = null
): ?string {
if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
$this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document');
throw new ObjectReadyException();
}
$this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [
'entity_id' => $entityId,
'destination_stored_object' => $destinationStoredObject === null ? null : $destinationStoredObject->getId()
]);
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
$entity = $this
->entityManager
->find($context->getEntityClass(), $entityId)
;
if (null === $entity) {
throw new RelatedEntityNotFoundException($template->getEntity(), $entityId);
}
$contextGenerationDataNormalized = array_merge(
$contextGenerationDataNormalized,
['creator' => $creator],
$context instanceof DocGeneratorContextWithPublicFormInterface ?
$context->contextGenerationDataDenormalize($template, $entity, $contextGenerationDataNormalized)
: []
);
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
$destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null;
$this->entityManager->clear();
gc_collect_cycles();
if (null !== $destinationStoredObjectId) {
$destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId);
}
if ($isTest && ($testFile instanceof File)) {
$templateDecrypted = file_get_contents($testFile->getPathname());
} else {
$templateDecrypted = $this->storedObjectManager->read($template->getFile());
}
try {
$generatedResource = $this
->driver
->generateFromString(
$templateDecrypted,
$template->getFile()->getType(),
$data,
$template->getFile()->getFilename()
);
} catch (TemplateException $e) {
throw new GeneratorException($e->getErrors(), $e);
}
if ($isTest) {
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
'is_test' => true,
'entity_id' => $entityId,
'destination_stored_object' => $destinationStoredObject === null ? null : $destinationStoredObject->getId()
]);
return $generatedResource;
}
/** @var StoredObject $storedObject */
$destinationStoredObject
->setType($template->getFile()->getType())
->setFilename(sprintf('%s_odt', uniqid('doc_', true)))
->setStatus(StoredObject::STATUS_READY)
;
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
$this->entityManager->flush();
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
'entity_id' => $entityId,
'destination_stored_object' => $destinationStoredObject->getId(),
]);
return null;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Service\Generator;
use RuntimeException;
use Throwable;
class GeneratorException extends RuntimeException
{
/**
* @var list<string>
*/
private array $errors;
public function __construct(array $errors = [], ?Throwable $previous = null)
{
$this->errors = $errors;
parent::__construct(
'Could not generate the document',
15252,
$previous
);
}
/**
* @return array
*/
public function getErrors(): array
{
return $this->errors;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Chill\DocGeneratorBundle\Service\Generator;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\User;
use Symfony\Component\HttpFoundation\File\File;
interface GeneratorInterface
{
/**
* @template T of File|null
* @template B of bool
* @param B $isTest
* @param (B is true ? T : null) $testFile
* @psalm-return (B is true ? string : null)
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
*/
public function generateDocFromTemplate(
DocGeneratorTemplate $template,
int $entityId,
array $contextGenerationDataNormalized,
?StoredObject $destinationStoredObject = null,
bool $isTest = false,
?File $testFile = null,
?User $creator = null
): ?string;
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Service\Generator;
use RuntimeException;
class ObjectReadyException extends RuntimeException
{
public function __construct()
{
parent::__construct('object is already ready', 6698856);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Service\Generator;
use RuntimeException;
class RelatedEntityNotFoundException extends RuntimeException
{
public function __construct(string $relatedEntityClass, int $relatedEntityId, ?Throwable $previous = null)
{
parent::__construct(
sprintf('Related entity not found: %s, %s', $relatedEntityClass, $relatedEntityId),
99876652,
$previous
);
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Contracts\Translation\TranslatorInterface;
final class OnGenerationFails implements EventSubscriberInterface
{
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
private EntityManagerInterface $entityManager;
private LoggerInterface $logger;
private MailerInterface $mailer;
private StoredObjectRepository $storedObjectRepository;
private TranslatorInterface $translator;
private UserRepositoryInterface $userRepository;
const LOG_PREFIX = '[docgen failed] ';
/**
* @param DocGeneratorTemplateRepository $docGeneratorTemplateRepository
* @param EntityManagerInterface $entityManager
* @param LoggerInterface $logger
* @param MailerInterface $mailer
* @param StoredObjectRepository $storedObjectRepository
* @param TranslatorInterface $translator
* @param UserRepositoryInterface $userRepository
*/
public function __construct(
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
EntityManagerInterface $entityManager,
LoggerInterface $logger,
MailerInterface $mailer,
StoredObjectRepository $storedObjectRepository,
TranslatorInterface $translator,
UserRepositoryInterface $userRepository
) {
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->entityManager = $entityManager;
$this->logger = $logger;
$this->mailer = $mailer;
$this->storedObjectRepository = $storedObjectRepository;
$this->translator = $translator;
$this->userRepository = $userRepository;
}
public static function getSubscribedEvents()
{
return [
WorkerMessageFailedEvent::class => 'onMessageFailed'
];
}
public function onMessageFailed(WorkerMessageFailedEvent $event): void
{
if ($event->willRetry()) {
return;
}
if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
return;
}
/** @var \Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage $message */
$message = $event->getEnvelope()->getMessage();
$this->logger->error(self::LOG_PREFIX.'Docgen failed', [
'stored_object_id' => $message->getDestinationStoredObjectId(),
'entity_id' => $message->getEntityId(),
'template_id' => $message->getTemplateId(),
'creator_id' => $message->getCreatorId(),
'throwable_class' => get_class($event->getThrowable()),
]);
$this->markObjectAsFailed($message);
$this->warnCreator($message, $event);
}
private function markObjectAsFailed(RequestGenerationMessage $message): void
{
$object = $this->storedObjectRepository->find($message->getDestinationStoredObjectId());
if (null === $object) {
$this->logger->error(self::LOG_PREFIX.'Stored object not found', ['stored_object_id', $message->getDestinationStoredObjectId()]);
}
$object->setStatus(StoredObject::STATUS_FAILURE);
$this->entityManager->flush();
}
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
{
if (null === $creatorId = $message->getCreatorId()) {
$this->logger->info(self::LOG_PREFIX.'creator id is null');
return;
}
if (null === $creator = $this->userRepository->find($creatorId)) {
$this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]);
return;
}
if (null === $creator->getEmail() || '' === $creator->getEmail()) {
$this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]);
return;
}
// if the exception is not a GeneratorException, we try the previous one...
$throwable = $event->getThrowable();
if (!$throwable instanceof GeneratorException) {
$throwable = $throwable->getPrevious();
}
if ($throwable instanceof GeneratorException) {
$errors = $throwable->getErrors();
} else {
$errors = [$throwable->getTraceAsString()];
}
if (null === $template = $this->docGeneratorTemplateRepository->find($message->getTemplateId())) {
$this->logger->info(self::LOG_PREFIX.'Template not found', ['template_id' => $message->getTemplateId()]);
return;
}
$email = (new TemplatedEmail())
->to($creator->getEmail())
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
->context([
'errors' => $errors,
'template' => $template,
'creator' => $creator,
'stored_object_id' => $message->getDestinationStoredObjectId(),
]);
$this->mailer->send($email);
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocGeneratorBundle\Service\Generator\Generator;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
/**
* Handle the request of document generation
*/
class RequestGenerationHandler implements MessageHandlerInterface
{
private StoredObjectRepository $storedObjectRepository;
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
private EntityManagerInterface $entityManager;
private Generator $generator;
private LoggerInterface $logger;
private UserRepositoryInterface $userRepository;
public const AUTHORIZED_TRIALS = 5;
private const LOG_PREFIX = '[docgen message handler] ';
public function __construct(
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
EntityManagerInterface $entityManager,
Generator $generator,
LoggerInterface $logger,
StoredObjectRepository $storedObjectRepository,
UserRepositoryInterface $userRepository
) {
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->entityManager = $entityManager;
$this->generator = $generator;
$this->logger = $logger;
$this->storedObjectRepository = $storedObjectRepository;
$this->userRepository = $userRepository;
}
public function __invoke(RequestGenerationMessage $message)
{
if (null === $template = $this->docGeneratorTemplateRepository->find($message->getTemplateId())) {
throw new \RuntimeException('template not found: ' . $message->getTemplateId());
}
if (null === $destinationStoredObject = $this->storedObjectRepository->find($message->getDestinationStoredObjectId())) {
throw new \RuntimeException('destination stored object not found : ' . $message->getDestinationStoredObjectId());
}
if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) {
throw new UnrecoverableMessageHandlingException('maximum number of retry reached');
}
$creator = $this->userRepository->find($message->getCreatorId());
$destinationStoredObject->addGenerationTrial();
$this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id')
->setParameter('id', $destinationStoredObject->getId())
->execute();
$this->generator->generateDocFromTemplate(
$template,
$message->getEntityId(),
$message->getContextGenerationData(),
$destinationStoredObject,
false,
null,
$creator
);
$this->logger->info(self::LOG_PREFIX.'Request generation finished', [
'template_id' => $message->getTemplateId(),
'destination_stored_object' => $message->getDestinationStoredObjectId(),
'duration_int' => (new \DateTimeImmutable('now'))->getTimestamp() - $message->getCreatedAt()->getTimestamp(),
]);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\User;
class RequestGenerationMessage
{
private int $creatorId;
private int $templateId;
private int $entityId;
private int $destinationStoredObjectId;
private array $contextGenerationData;
private \DateTimeImmutable $createdAt;
public function __construct(
User $creator,
DocGeneratorTemplate $template,
int $entityId,
StoredObject $destinationStoredObject,
array $contextGenerationData
) {
$this->creatorId = $creator->getId();
$this->templateId = $template->getId();
$this->entityId = $entityId;
$this->destinationStoredObjectId = $destinationStoredObject->getId();
$this->contextGenerationData = $contextGenerationData;
$this->createdAt = new \DateTimeImmutable('now');
}
public function getCreatorId(): int
{
return $this->creatorId;
}
public function getDestinationStoredObjectId(): int
{
return $this->destinationStoredObjectId;
}
public function getTemplateId(): int
{
return $this->templateId;
}
public function getEntityId(): int
{
return $this->entityId;
}
public function getContextGenerationData(): array
{
return $this->contextGenerationData;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
}

View File

@@ -20,10 +20,14 @@ services:
resource: '../Serializer/Normalizer/'
tags:
- { name: 'serializer.normalizer', priority: -152 }
Chill\DocGeneratorBundle\Serializer\Normalizer\CollectionDocGenNormalizer:
tags:
- { name: 'serializer.normalizer', priority: -126 }
Chill\DocGeneratorBundle\Service\Context\:
resource: "../Service/Context"
Chill\DocGeneratorBundle\Controller\:
resource: "../Controller"
autowire: true
@@ -34,18 +38,20 @@ services:
autowire: true
autoconfigure: true
Chill\DocGeneratorBundle\Service\Context\:
resource: "../Service/Context/"
autowire: true
autoconfigure: true
Chill\DocGeneratorBundle\GeneratorDriver\:
resource: "../GeneratorDriver/"
autowire: true
autoconfigure: true
Chill\DocGeneratorBundle\Service\Messenger\:
resource: "../Service/Messenger/"
Chill\DocGeneratorBundle\Service\Generator\Generator: ~
Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface: '@Chill\DocGeneratorBundle\Service\Generator\Generator'
Chill\DocGeneratorBundle\Driver\RelatorioDriver: '@Chill\DocGeneratorBundle\Driver\DriverInterface'
Chill\DocGeneratorBundle\Context\ContextManager:
arguments:
$contexts: !tagged_iterator { tag: chill_docgen.context, default_index_method: getKey }
Chill\DocGeneratorBundle\Context\ContextManagerInterface: '@Chill\DocGeneratorBundle\Context\ContextManager'

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\DocGenerator;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20230214192558 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add status, template_id and fix defaults on chill_doc.stored_object';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_doc.stored_object ADD template_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_doc.stored_object ADD status TEXT DEFAULT \'ready\' NOT NULL');
$this->addSql('ALTER TABLE chill_doc.stored_object ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('UPDATE chill_doc.stored_object SET createdAt = creation_date');
$this->addSql('ALTER TABLE chill_doc.stored_object ADD createdBy_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_doc.stored_object DROP creation_date;');
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER type SET DEFAULT \'\'');
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER title DROP DEFAULT');
$this->addSql('COMMENT ON COLUMN chill_doc.stored_object.createdAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_doc.stored_object ADD CONSTRAINT FK_49604E365DA0FB8 FOREIGN KEY (template_id) REFERENCES chill_docgen_template (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_doc.stored_object ADD CONSTRAINT FK_49604E363174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_49604E365DA0FB8 ON chill_doc.stored_object (template_id)');
$this->addSql('CREATE INDEX IDX_49604E363174800F ON chill_doc.stored_object (createdBy_id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_doc.stored_object DROP CONSTRAINT FK_49604E365DA0FB8');
$this->addSql('ALTER TABLE chill_doc.stored_object DROP CONSTRAINT FK_49604E363174800F');
$this->addSql('ALTER TABLE chill_doc.stored_object DROP template_id');
$this->addSql('ALTER TABLE chill_doc.stored_object DROP status');
$this->addSql('ALTER TABLE chill_doc.stored_object ADD creation_date TIMESTAMP(0) DEFAULT NOW()');
$this->addSql('UPDATE chill_doc.stored_object SET creation_date = createdAt');
$this->addSql('ALTER TABLE chill_doc.stored_object DROP createdAt');
$this->addSql('ALTER TABLE chill_doc.stored_object DROP createdBy_id');
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER title SET DEFAULT \'\'');
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER type DROP DEFAULT');
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Chill\DocGeneratorBundle\tests\Service\Context\Generator;
use Chill\DocGeneratorBundle\Context\ContextManagerInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextInterface;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
use Chill\DocGeneratorBundle\Service\Generator\Generator;
use Chill\DocGeneratorBundle\Service\Generator\ObjectReadyException;
use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
class GeneratorTest extends TestCase
{
use ProphecyTrait;
public function testSuccessfulGeneration(): void
{
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
->setType('application/test'));
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING);
$reflection = new \ReflectionClass($destinationStoredObject);
$reflection->getProperty('id')->setAccessible(true);
$reflection->getProperty('id')->setValue($destinationStoredObject, 1);
$entity = new class {};
$data = [];
$context = $this->prophesize(DocGeneratorContextInterface::class);
$context->getData($template, $entity, Argument::type('array'))->willReturn($data);
$context->getName()->willReturn('dummy_context');
$context->getEntityClass()->willReturn('DummyClass');
$context = $context->reveal();
$contextManagerInterface = $this->prophesize(ContextManagerInterface::class);
$contextManagerInterface->getContextByDocGeneratorTemplate($template)
->willReturn($context);
$driver = $this->prophesize(DriverInterface::class);
$driver->generateFromString('template', 'application/test', $data, Argument::any())
->willReturn('generated');
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->find(StoredObject::class, 1)
->willReturn($destinationStoredObject);
$entityManager->find('DummyClass', Argument::type('int'))
->willReturn($entity);
$entityManager->clear()->shouldBeCalled();
$entityManager->flush()->shouldBeCalled();
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
$storedObjectManager->read($templateStoredObject)->willReturn('template');
$storedObjectManager->write($destinationStoredObject, 'generated')->shouldBeCalled();
$generator = new Generator(
$contextManagerInterface->reveal(),
$driver->reveal(),
$entityManager->reveal(),
new NullLogger(),
$storedObjectManager->reveal()
);
$generator->generateDocFromTemplate(
$template,
1,
[],
$destinationStoredObject
);
}
public function testPreventRegenerateDocument(): void
{
$this->expectException(ObjectReadyException::class);
$generator = new Generator(
$this->prophesize(ContextManagerInterface::class)->reveal(),
$this->prophesize(DriverInterface::class)->reveal(),
$this->prophesize(EntityManagerInterface::class)->reveal(),
new NullLogger(),
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
);
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
->setType('application/test'));
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_READY);
$generator->generateDocFromTemplate(
$template,
1,
[],
$destinationStoredObject
);
}
public function testRelatedEntityNotFound(): void
{
$this->expectException(RelatedEntityNotFoundException::class);
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
->setType('application/test'));
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING);
$reflection = new \ReflectionClass($destinationStoredObject);
$reflection->getProperty('id')->setAccessible(true);
$reflection->getProperty('id')->setValue($destinationStoredObject, 1);
$context = $this->prophesize(DocGeneratorContextInterface::class);
$context->getName()->willReturn('dummy_context');
$context->getEntityClass()->willReturn('DummyClass');
$context = $context->reveal();
$contextManagerInterface = $this->prophesize(ContextManagerInterface::class);
$contextManagerInterface->getContextByDocGeneratorTemplate($template)
->willReturn($context);
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->find(Argument::type('string'), Argument::type('int'))
->willReturn(null);
$generator = new Generator(
$contextManagerInterface->reveal(),
$this->prophesize(DriverInterface::class)->reveal(),
$entityManager->reveal(),
new NullLogger(),
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
);
$generator->generateDocFromTemplate(
$template,
1,
[],
$destinationStoredObject
);
}
}

View File

@@ -10,6 +10,16 @@ docgen:
test generate: Tester la génération
With context %name%: 'Avec le contexte "%name%"'
Doc generation failed: La génération de ce document a échoué
Doc generation is pending: La génération de ce document est en cours
Come back later: Revenir plus tard
failure_email:
The generation of a document failed: La génération d'un document a échoué
The generation of the document {template_name} failed: La génération d'un document à partir du modèle {{ template_name }} a échoué.
The following errors were encoutered: Les erreurs suivantes ont été rencontrées
Forward this email to your administrator for solving: Faites suivre ce message vers votre administrateur pour la résolution du problème.
References: Références
crud:
docgen_template:
@@ -19,4 +29,4 @@ crud:
Show data instead of generating: Montrer les données au lieu de générer le document
Template file: Fichier modèle
Template file: Fichier modèle

View File

@@ -0,0 +1,39 @@
<?php
namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
class StoredObjectApiController
{
private Security $security;
public function __construct(Security $security)
{
$this->security = $security;
}
/**
* @Route("/api/1.0/doc-store/stored-object/{uuid}/is-ready")
*/
public function isDocumentReady(StoredObject $storedObject): Response
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
return new JsonResponse(
[
'id' => $storedObject->getId(),
'filename' => $storedObject->getFilename(),
'status' => $storedObject->getStatus(),
'type' => $storedObject->getType(),
]
);
}
}

View File

@@ -14,6 +14,9 @@ namespace Chill\DocStoreBundle\Entity;
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists;
use ChampsLibres\WopiLib\Contract\Entity\Document;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use DateTime;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
@@ -30,13 +33,13 @@ use Symfony\Component\Serializer\Annotation as Serializer;
* message="The file is not stored properly"
* )
*/
class StoredObject implements AsyncFileInterface, Document
class StoredObject implements AsyncFileInterface, Document, TrackCreationInterface
{
/**
* @ORM\Column(type="datetime", name="creation_date")
* @Serializer\Groups({"read", "write"})
*/
private DateTimeInterface $creationDate;
public const STATUS_READY = "ready";
public const STATUS_PENDING = "pending";
public const STATUS_FAILURE = "failure";
use TrackCreationTrait;
/**
* @ORM\Column(type="json", name="datas")
@@ -48,7 +51,7 @@ class StoredObject implements AsyncFileInterface, Document
* @ORM\Column(type="text")
* @Serializer\Groups({"read", "write"})
*/
private $filename;
private string $filename = '';
/**
* @ORM\Id
@@ -56,7 +59,7 @@ class StoredObject implements AsyncFileInterface, Document
* @ORM\Column(type="integer")
* @Serializer\Groups({"read", "write"})
*/
private $id;
private ?int $id;
/**
* @var int[]
@@ -78,7 +81,7 @@ class StoredObject implements AsyncFileInterface, Document
private string $title = '';
/**
* @ORM\Column(type="text", name="type")
* @ORM\Column(type="text", name="type", options={"default": ""})
* @Serializer\Groups({"read", "write"})
*/
private string $type = '';
@@ -89,28 +92,68 @@ class StoredObject implements AsyncFileInterface, Document
*/
private UuidInterface $uuid;
public function __construct()
/**
* @ORM\ManyToOne(targetEntity=DocGeneratorTemplate::class)
*/
private ?DocGeneratorTemplate $template;
/**
* @ORM\Column(type="text", options={"default": "ready"})
* @Serializer\Groups({"read"})
*/
private string $status;
/**
* Store the number of times a generation has been tryied for this StoredObject.
*
* This is a workaround, as generation consume lot of memory, and out-of-memory errors
* are not handled by messenger.
*
* @ORM\Column(type="integer", options={"default": 0})
*/
private int $generationTrialsCounter = 0;
/**
* @param StoredObject::STATUS_* $status
*/
public function __construct(string $status = "ready")
{
$this->creationDate = new DateTime();
$this->uuid = Uuid::uuid4();
$this->status = $status;
}
public function addGenerationTrial(): self
{
$this->generationTrialsCounter++;
return $this;
}
/**
* @Serializer\Groups({"read", "write"})
* @deprecated
*/
public function getCreationDate(): DateTime
{
return $this->creationDate;
return DateTime::createFromImmutable($this->createdAt);
}
public function getDatas()
public function getDatas(): array
{
return $this->datas;
}
public function getFilename()
public function getFilename(): string
{
return $this->filename;
}
public function getId()
public function getGenerationTrialsCounter(): int
{
return $this->generationTrialsCounter;
}
public function getId(): ?int
{
return $this->id;
}
@@ -133,6 +176,14 @@ class StoredObject implements AsyncFileInterface, Document
return $this->getFilename();
}
/**
* @return StoredObject::STATUS_*
*/
public function getStatus(): string
{
return $this->status;
}
public function getTitle()
{
return $this->title;
@@ -153,52 +204,92 @@ class StoredObject implements AsyncFileInterface, Document
return (string) $this->uuid;
}
public function setCreationDate(DateTime $creationDate)
/**
* @Serializer\Groups({"write"})
* @deprecated
*/
public function setCreationDate(DateTime $creationDate): self
{
$this->creationDate = $creationDate;
$this->createdAt = \DateTimeImmutable::createFromMutable($creationDate);
return $this;
}
public function setDatas(?array $datas)
public function setDatas(?array $datas): self
{
$this->datas = (array) $datas;
return $this;
}
public function setFilename(?string $filename)
public function setFilename(?string $filename): self
{
$this->filename = (string) $filename;
return $this;
}
public function setIv(?array $iv)
public function setIv(?array $iv): self
{
$this->iv = (array) $iv;
return $this;
}
public function setKeyInfos(?array $keyInfos)
public function setKeyInfos(?array $keyInfos): self
{
$this->keyInfos = (array) $keyInfos;
return $this;
}
public function setTitle(?string $title)
/**
* @param StoredObject::STATUS_* $status
*/
public function setStatus(string $status): self
{
$this->status = $status;
return $this;
}
public function setTitle(?string $title): self
{
$this->title = (string) $title;
return $this;
}
public function setType(?string $type)
public function setType(?string $type): self
{
$this->type = (string) $type;
return $this;
}
public function getTemplate(): ?DocGeneratorTemplate
{
return $this->template;
}
public function hasTemplate(): bool
{
return null !== $this->template;
}
public function setTemplate(?DocGeneratorTemplate $template): StoredObject
{
$this->template = $template;
return $this;
}
public function isPending(): bool
{
return self::STATUS_PENDING === $this->getStatus();
}
public function isFailure(): bool
{
return self::STATUS_FAILURE === $this->getStatus();
}
}

View File

@@ -1,7 +1,8 @@
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
import {createApp} from "vue";
import {StoredObject} from "../../types";
import {StoredObject, StoredObjectStatusChange} from "../../types";
import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers";
const i18n = _createI18n({});
@@ -15,19 +16,32 @@ window.addEventListener('DOMContentLoaded', function (e) {
filename: string,
canEdit: string,
storedObject: string,
small: string,
buttonSmall: string,
};
const
storedObject = JSON.parse(datasets.storedObject),
storedObject = JSON.parse(datasets.storedObject) as StoredObject,
filename = datasets.filename,
canEdit = datasets.canEdit === '1',
small = datasets.small === '1'
small = datasets.buttonSmall === '1'
;
return { storedObject, filename, canEdit, small };
},
template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small"></document-action-buttons-group>',
template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
methods: {
onStoredObjectStatusChange: function(newStatus: StoredObjectStatusChange): void {
this.$data.storedObject.status = newStatus.status;
this.$data.storedObject.filename = newStatus.filename;
this.$data.storedObject.type = newStatus.type;
// remove eventual div which inform pending status
document.querySelectorAll(`[data-docgen-is-pending="${this.$data.storedObject.id}"]`)
.forEach(function(el) {
el.remove();
});
}
}
});
app.use(i18n).mount(el);

View File

@@ -1,5 +1,7 @@
import {DateTime} from "../../../ChillMainBundle/Resources/public/types";
export type StoredObjectStatus = "ready"|"failure"|"pending";
export interface StoredObject {
id: number,
@@ -13,7 +15,15 @@ export interface StoredObject {
keyInfos: object,
title: string,
type: string,
uuid: string
uuid: string,
status: StoredObjectStatus,
}
export interface StoredObjectStatusChange {
id: number,
filename: string,
status: StoredObjectStatus,
type: string,
}
/**

View File

@@ -1,6 +1,6 @@
<template>
<div class="dropdown">
<button :class="Object.assign({'btn': true, 'btn-outline-primary': true, 'dropdown-toggle': true, small: props.small})" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<div v-if="'ready' === props.storedObject.status" class="btn-group">
<button :class="Object.assign({'btn': true, 'btn-outline-primary': true, 'dropdown-toggle': true, 'btn-sm': props.small})" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Actions
</button>
<ul class="dropdown-menu">
@@ -15,16 +15,27 @@
</li>
</ul>
</div>
<div v-else-if="'pending' === props.storedObject.status">
<div class="btn btn-outline-info">Génération en cours</div>
</div>
<div v-else-if="'failure' === props.storedObject.status">
<div class="btn btn-outline-danger">La génération a échoué</div>
</div>
</template>
<script lang="ts" setup>
import {onMounted} from "vue";
import ConvertButton from "./StoredObjectButton/ConvertButton.vue";
import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
import {is_extension_editable, is_extension_viewable} from "./StoredObjectButton/helpers";
import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../types";
import {is_extension_editable, is_extension_viewable, is_object_ready} from "./StoredObjectButton/helpers";
import {
StoredObject,
StoredObjectStatusChange,
WopiEditButtonExecutableBeforeLeaveFunction
} from "../types";
interface DocumentActionButtonsGroupConfig {
storedObject: StoredObject,
@@ -48,6 +59,10 @@ interface DocumentActionButtonsGroupConfig {
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
}
const emit = defineEmits<{
(e: 'onStoredObjectStatusChange', newStatus: StoredObjectStatusChange): void
}>();
const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
small: false,
canEdit: true,
@@ -56,6 +71,51 @@ const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
returnPath: window.location.pathname + window.location.search + window.location.hash,
});
/**
* counter for the number of times that we check for a new status
*/
let tryiesForReady = 0;
/**
* how many times we may check for a new status, once loaded
*/
const maxTryiesForReady = 120;
const checkForReady = function(): void {
if (
'ready' === props.storedObject.status
|| 'failure' === props.storedObject.status
// stop reloading if the page stays opened for a long time
|| tryiesForReady > maxTryiesForReady
) {
return;
}
tryiesForReady = tryiesForReady + 1;
setTimeout(onObjectNewStatusCallback, 5000);
};
const onObjectNewStatusCallback = async function(): Promise<void> {
const new_status = await is_object_ready(props.storedObject);
if (props.storedObject.status !== new_status.status) {
emit('onStoredObjectStatusChange', new_status);
return Promise.resolve();
} else if ('failure' === new_status.status) {
return Promise.resolve();
}
if ('ready' !== new_status.status) {
// we check for new status, unless it is ready
checkForReady();
}
return Promise.resolve();
};
onMounted(() => {
checkForReady();
})
</script>
<style scoped>

View File

@@ -38,13 +38,11 @@ async function download_and_open(event: Event): Promise<void> {
button.href = window.URL.createObjectURL(raw);
button.type = props.storedObject.type;
if (props.filename !== undefined) {
button.download = props.filename || 'document';
button.download = props.filename || 'document';
const ext = mime.getExtension(props.storedObject.type);
if (null !== ext) {
button.download = button.download + '.' + ext;
}
const ext = mime.getExtension(props.storedObject.type);
if (null !== ext) {
button.download = button.download + '.' + ext;
}
}

View File

@@ -1,3 +1,4 @@
import {StoredObject, StoredObjectStatus, StoredObjectStatusChange} from "../../types";
const MIMES_EDIT = new Set([
'application/vnd.ms-powerpoint',
@@ -168,6 +169,18 @@ async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKe
}
}
async function is_object_ready(storedObject: StoredObject): Promise<StoredObjectStatusChange>
{
const new_status_response = await window
.fetch( `/api/1.0/doc-store/stored-object/${storedObject.uuid}/is-ready`);
if (!new_status_response.ok) {
throw new Error("could not fetch the new status");
}
return await new_status_response.json();
}
export {
build_convert_link,
build_download_info_link,
@@ -176,4 +189,5 @@ export {
download_doc,
is_extension_editable,
is_extension_viewable,
is_object_ready,
};

View File

@@ -9,11 +9,6 @@
<dt>{{ 'Title'|trans }}</dt>
<dd>{{ document.title }}</dd>
{% if document.scope is not null %}
<dt>{{ 'Scope' | trans }}</dt>
<dd>{{ document.scope.name | localize_translatable_string }}</dd>
{% endif %}
<dt>{{ 'Category'|trans }}</dt>
<dd>{{ document.category.name|localize_translatable_string }}</dd>

View File

@@ -5,18 +5,25 @@
<div class="item-bloc">
<div class="item-row">
<div class="item-col" style="width: unset">
{% if document.object.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.object.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.object.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
<div class="denomination h2">
{{ document.title }}
</div>
<div>
{{ mm.mimeIcon(document.object.type) }}
</div>
{% if document.object.type is not empty %}
<div>
{{ mm.mimeIcon(document.object.type) }}
</div>
{% endif %}
<div>
<p>{{ document.category.name|localize_translatable_string }}</p>
</div>
{% if document.template is not null %}
{% if document.object.hasTemplate %}
<div>
<p>{{ document.template.name.fr }}</p>
<p>{{ document.object.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>

View File

@@ -157,7 +157,7 @@ final class WopiEditTwigExtensionRuntime implements RuntimeExtensionInterface
'document_json' => $this->normalizer->normalize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]),
'title' => $title,
'can_edit' => $canEdit,
'options' => array_merge($options, self::DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP),
'options' => array_merge(self::DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP, $options),
]);
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\DocStore;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20230227161327 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add a generation counter on doc store';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_doc.stored_object ADD generationTrialsCounter INT DEFAULT 0 NOT NULL;');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_doc.stored_object DROP generationTrialsCounter');
}
}

View File

@@ -19,6 +19,9 @@ The document is successfully registered: Le document est enregistré
The document is successfully updated: Le document est mis à jour
Any description: Aucune description
document:
Any title: Aucun titre
# delete
Delete document ?: Supprimer le document ?
Are you sure you want to remove this document ?: Êtes-vous sûr·e de vouloir supprimer ce document ?
@@ -73,4 +76,4 @@ CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE: Créer un document
CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE: Supprimer un document
CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE: Voir les documents
CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS: Voir les détails d'un document
CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE: Modifier un document
CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE: Modifier un document

View File

@@ -72,7 +72,6 @@ class ChillMainBundle extends Bundle
$container->addCompilerPass(new NotificationCounterCompilerPass());
$container->addCompilerPass(new MenuCompilerPass());
$container->addCompilerPass(new ACLFlagsCompilerPass());
$container->addCompilerPass(new GroupingCenterCompilerPass());
$container->addCompilerPass(new CRUDControllerCompilerPass());
$container->addCompilerPass(new ShortMessageCompilerPass());
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Form\AbsenceType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class AbsenceController extends AbstractController
{
/**
* @Route(
* "/{_locale}/absence",
* name="chill_main_user_absence_index",
* methods={"GET", "POST"}
* )
*/
public function setAbsence(Request $request)
{
$user = $this->getUser();
$form = $this->createForm(AbsenceType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->flush();
return $this->redirect($this->generateUrl('chill_main_user_absence_index'));
}
return $this->render('@ChillMain/Menu/absence.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
/**
* @Route(
* "/{_locale}/absence/unset",
* name="chill_main_user_absence_unset",
* methods={"GET", "POST"}
* )
*/
public function unsetAbsence(Request $request)
{
$user = $this->getUser();
$user->setAbsenceStart(null);
$em = $this->getDoctrine()->getManager();
$em->flush();
return $this->redirect($this->generateUrl('chill_main_user_absence_index'));
}
}

View File

@@ -298,6 +298,8 @@ class ExportController extends AbstractController
'csrf_protection' => $isGenerate ? false : true,
]);
// TODO: add a condition to be able to select a regroupment of centers?
if ('centers' === $step || 'generate_centers' === $step) {
$builder->add('centers', PickCenterType::class, [
'export_alias' => $alias,

View File

@@ -28,7 +28,7 @@ class LocationController extends CRUDController
protected function customizeQuery(string $action, Request $request, $query): void
{
$query->where('e.availableForUsers = "TRUE"');
$query->where('e.availableForUsers = TRUE');
}
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)

View File

@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\DependencyInjection\CompilerPass;
use LogicException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class GroupingCenterCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (false === $container->hasDefinition('chill.main.form.pick_centers_type')) {
throw new LogicException('The service chill.main.form.pick_centers_type does '
. 'not exists in container');
}
$pickCenterType = $container->getDefinition('chill.main.form.pick_centers_type');
foreach ($container->findTaggedServiceIds('chill.grouping_center') as $serviceId => $tagged) {
$pickCenterType->addMethodCall(
'addGroupingCenter',
[new Reference($serviceId)]
);
}
}
}

View File

@@ -24,7 +24,7 @@ class Regroupment
/**
* @var Center
* @ORM\ManyToMany(
* targetEntity="Chill\MainBundle\Entity\Center"
* targetEntity=Center::class
* )
* @ORM\Id
*/
@@ -43,7 +43,7 @@ class Regroupment
private bool $isActive = true;
/**
* @ORM\Column(type="string", length=15, options={"default": ""}, nullable=false)
* @ORM\Column(type="text", options={"default": ""}, nullable=false)
*/
private string $name = '';
@@ -52,7 +52,7 @@ class Regroupment
$this->centers = new ArrayCollection();
}
public function getCenters(): ?Collection
public function getCenters(): Collection
{
return $this->centers;
}

View File

@@ -11,14 +11,15 @@ declare(strict_types=1);
namespace Chill\MainBundle\Entity;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use RuntimeException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use function in_array;
/**
@@ -40,6 +41,11 @@ class User implements UserInterface
*/
protected ?int $id = null;
/**
* @ORM\Column(type="datetime_immutable", nullable=true)
*/
private ?DateTimeImmutable $absenceStart = null;
/**
* Array where SAML attributes's data are stored.
*
@@ -173,6 +179,11 @@ class User implements UserInterface
{
}
public function getAbsenceStart(): ?DateTimeImmutable
{
return $this->absenceStart;
}
/**
* Get attributes.
*
@@ -291,6 +302,11 @@ class User implements UserInterface
return $this->usernameCanonical;
}
public function isAbsent(): bool
{
return null !== $this->getAbsenceStart() && $this->getAbsenceStart() <= new DateTimeImmutable('now');
}
/**
* @return bool
*/
@@ -355,6 +371,11 @@ class User implements UserInterface
}
}
public function setAbsenceStart(?DateTimeImmutable $absenceStart): void
{
$this->absenceStart = $absenceStart;
}
public function setAttributeByDomain(string $domain, string $key, $value): self
{
$this->attributes[$domain][$key] = $value;

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Export;
use Closure;
use Doctrine\ORM\QueryBuilder;
/**

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Export\Helper;
use DateTime;
use DateTimeInterface;
use Exception;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,7 +36,7 @@ class DateTimeHelper
return '';
}
if ($value instanceof \DateTimeInterface) {
if ($value instanceof DateTimeInterface) {
return $value;
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ChillDateType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AbsenceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('absenceStart', ChillDateType::class, [
'required' => true,
'input' => 'datetime_immutable',
'label' => 'absence.Absence start',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form\DataMapper;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Repository\RegroupmentRepository;
use Exception;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\FormInterface;
use function array_key_exists;
use function count;
class ExportPickCenterDataMapper implements DataMapperInterface
{
protected RegroupmentRepository $regroupmentRepository;
/**
* @param array|Center[] $data
* @param $forms
*
* @throws Exception
*
* @return void
*/
public function mapDataToForms($data, $forms)
{
if (null === $data) {
return;
}
/** @var array<string, FormInterface> $form */
$form = iterator_to_array($forms);
$pickedRegroupment = [];
foreach ($this->regroupmentRepository->findAll() as $regroupment) {
[$contained, $notContained] = $regroupment->getCenters()->partition(static function (Center $center) {
});
if (0 === count($notContained)) {
$pickedRegroupment[] = $regroupment;
}
}
$form['regroupment']->setData($pickedRegroupment);
$form['centers']->setData($data);
}
/**
* @param iterable $forms
* @param array $data
*
* @return void
*/
public function mapFormsToData($forms, &$data)
{
/** @var array<string, FormInterface> $forms */
$forms = iterator_to_array($forms);
$centers = [];
foreach ($forms['center']->getData() as $center) {
$centers[spl_object_hash($center)] = $center;
}
if (array_key_exists('regroupment', $forms)) {
foreach ($forms['regroupment']->getData() as $regroupment) {
/** @var Regroupment $regroupment */
foreach ($regroupment->getCenters() as $center) {
$centers[spl_object_hash($center)] = $center;
}
}
}
$data = array_values($centers);
}
}

View File

@@ -11,57 +11,46 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Center\GroupingCenterInterface;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Form\DataMapper\ExportPickCenterDataMapper;
use Chill\MainBundle\Repository\RegroupmentRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use function array_intersect;
use function array_key_exists;
use function array_merge;
use function array_unique;
use function count;
use function in_array;
/**
* Pick centers amongst available centers for the user.
*/
class PickCenterType extends AbstractType
final class PickCenterType extends AbstractType
{
public const CENTERS_IDENTIFIERS = 'c';
protected AuthorizationHelperInterface $authorizationHelper;
private AuthorizationHelperInterface $authorizationHelper;
protected ExportManager $exportManager;
private ExportManager $exportManager;
/**
* @var array|GroupingCenterInterface[]
*/
protected array $groupingCenters = [];
private RegroupmentRepository $regroupmentRepository;
protected UserInterface $user;
private UserInterface $user;
public function __construct(
TokenStorageInterface $tokenStorage,
ExportManager $exportManager,
RegroupmentRepository $regroupmentRepository,
AuthorizationHelperInterface $authorizationHelper
) {
$this->exportManager = $exportManager;
$this->user = $tokenStorage->getToken()->getUser();
$this->authorizationHelper = $authorizationHelper;
}
public function addGroupingCenter(GroupingCenterInterface $grouping)
{
$this->groupingCenters[md5($grouping->getName())] = $grouping;
$this->regroupmentRepository = $regroupmentRepository;
}
public function buildForm(FormBuilderInterface $builder, array $options)
@@ -72,97 +61,36 @@ class PickCenterType extends AbstractType
$export->requiredRole()
);
$builder->add(self::CENTERS_IDENTIFIERS, EntityType::class, [
$builder->add('center', EntityType::class, [
'class' => Center::class,
'label' => 'center',
'choices' => $centers,
'multiple' => true,
'expanded' => true,
'choice_label' => static function (Center $c) {
return $c->getName();
},
'data' => count($this->groupingCenters) > 0 ? null : $centers,
'data' => $centers,
]);
if (count($this->groupingCenters) > 0) {
$groupingBuilder = $builder->create('g', null, [
'compound' => true,
if (count($this->regroupmentRepository->findAllActive()) > 0) {
$builder->add('regroupment', EntityType::class, [
'class' => Regroupment::class,
'label' => 'regroupment',
'multiple' => true,
'expanded' => true,
'choices' => $this->regroupmentRepository->findAllActive(),
'choice_label' => static function (Regroupment $r) {
return $r->getName();
},
]);
foreach ($this->groupingCenters as $key => $gc) {
$choices = $this->buildChoices($centers, $gc);
if (count($choices) > 0) {
$groupingBuilder->add($key, ChoiceType::class, [
'choices' => $choices,
'multiple' => true,
'expanded' => true,
'label' => $gc->getName(),
'required' => false,
]);
}
}
if ($groupingBuilder->count() > 0) {
$builder->add($groupingBuilder);
}
}
$builder->addModelTransformer(new CallbackTransformer(
function ($data) use ($centers) {
return $this->transform($data, $centers);
},
function ($data) use ($centers) {
return $this->reverseTransform($data, $centers);
}
));
$builder->setDataMapper(new ExportPickCenterDataMapper());
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired('export_alias');
}
protected function buildChoices($reachablesCenters, GroupingCenterInterface $gc)
{
$result = [];
foreach ($gc->getGroups() as $group) {
foreach ($gc->getCentersForGroup($group) as $center) {
if (in_array($center, $reachablesCenters, true)) {
$result[$group] = $group;
}
}
}
return $result;
}
protected function reverseTransform($data, $centers)
{
$picked = $data[self::CENTERS_IDENTIFIERS]
instanceof \Doctrine\Common\Collections\Collection ?
$data[self::CENTERS_IDENTIFIERS]->toArray()
:
$data[self::CENTERS_IDENTIFIERS];
if (array_key_exists('g', $data)) {
foreach ($data['g'] as $gcid => $group) {
$picked =
array_merge(
array_intersect(
$this->groupingCenters[$gcid]->getCentersForGroup($group),
$centers
),
$picked
);
}
}
return array_unique($picked);
}
protected function transform($data, $centers)
{
return $data;
}
}

View File

@@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\PickCivilityType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityRepository;
@@ -110,6 +111,11 @@ class UserType extends AbstractType
return $qb;
},
])
->add('absenceStart', ChillDateType::class, [
'required' => false,
'input' => 'datetime_immutable',
'label' => 'absence.Absence start',
]);
// @phpstan-ignore-next-line

View File

@@ -118,7 +118,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
* Return true if the phonenumber is a landline or voip phone. Return always true
* if the validation is not configured.
*
* @param string $phonenumber
* @param string|PhoneNumber $phonenumber
*/
public function isValidPhonenumberAny($phonenumber): bool
{
@@ -138,7 +138,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
* Return true if the phonenumber is a landline or voip phone. Return always true
* if the validation is not configured.
*
* @param string $phonenumber
* @param string|PhoneNumber $phonenumber
*/
public function isValidPhonenumberLandOrVoip($phonenumber): bool
{
@@ -159,7 +159,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
* REturn true if the phonenumber is a mobile phone. Return always true
* if the validation is not configured.
*
* @param string $phonenumber
* @param string|PhoneNumber $phonenumber
*/
public function isValidPhonenumberMobile($phonenumber): bool
{
@@ -182,6 +182,10 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
return null;
}
if ($phonenumber instanceof PhoneNumber) {
$phonenumber = (string) $phonenumber;
}
// filter only number
$filtered = preg_replace('/[^0-9]/', '', $phonenumber);

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Regroupment;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class RegroupmentRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(Regroupment::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?Regroupment
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
/**
* @return Regroupment[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
public function findAllActive(): array
{
return $this->repository->findBy(['isActive' => true], ['name' => 'ASC']);
}
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return Regroupment[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria, ?array $orderBy = null): ?Regroupment
{
return $this->repository->findOneBy($criteria, $orderBy);
}
public function getClassName()
{
return Regroupment::class;
}
}

View File

@@ -35,6 +35,7 @@ export interface User {
id: number;
username: string;
text: string;
text_without_absence: string;
email: string;
user_job: Job;
label: string;

View File

@@ -233,7 +233,7 @@ export default {
// console.log('data original', data);
data.parent = {type: "thirdparty", id: this.parent.id};
data.civility = data.civility !== null ? {type: 'chill_main_civility', id: data.civility.id} : null;
data.profession = data.profession !== null ? {type: 'third_party_profession', id: data.profession.id} : null;
data.profession = data.profession !== '' ? data.profession : '';
} else {
type = this.$refs.castNew.radioType;
data = this.$refs.castNew.castDataByType();
@@ -241,8 +241,8 @@ export default {
if (typeof data.civility !== 'undefined' && null !== data.civility) {
data.civility = data.civility !== null ? {type: 'chill_main_civility', id: data.civility.id} : null;
}
if (typeof data.profession !== 'undefined' && null !== data.profession) {
data.profession = data.profession !== null ? {type: 'third_party_profession', id: data.profession.id} : null;
if (typeof data.profession !== 'undefined' && '' !== data.profession) {
data.profession = data.profession !== '' ? data.profession : '';
}
// console.log('onthefly data', data);
}

View File

@@ -1,8 +1,7 @@
<template>
<span class="chill-entity entity-user">
{{ user.label }}
<span class="user-job" v-if="user.user_job !== null">({{ user.user_job.label.fr }})</span>
<span class="main-scope" v-if="user.main_scope !== null">({{ user.main_scope.name.fr }})</span>
<span class="user-job" v-if="user.user_job !== null">({{ user.user_job.label.fr }})</span> <span class="main-scope" v-if="user.main_scope !== null">({{ user.main_scope.name.fr }})</span> <span v-if="user.isAbsent" class="badge bg-danger rounded-pill" :title="Absent">A</span>
</span>
</template>

View File

@@ -6,4 +6,7 @@
{%- if opts['main_scope'] and user.mainScope is not null %}
<span class="main-scope">({{ user.mainScope.name|localize_translatable_string }})</span>
{%- endif -%}
{%- if opts['absence'] and user.isAbsent %}
<span class="badge bg-danger rounded-pill" title="{{ 'absence.Absent'|trans|escape('html_attr') }}">{{ 'absence.A'|trans }}</span>
{%- endif -%}
</span>

View File

@@ -1,5 +1,5 @@
{#
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
<info@champs-libres.coop> / <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
@@ -22,39 +22,34 @@
{% block content %}
<div class="col-md-10">
{{ include('@ChillMain/Export/_breadcrumb.html.twig') }}
<h1>{{ export.title|trans }}</h1>
<p>{{ export.description|trans }}</p>
{{ form_start(form) }}
<section class="center mb-4">
<h2>{{ 'Pick centers'|trans }}</h2>
<p>{{ 'The export will contains only data from the picked centers.'|trans }}
{{ 'This will eventually restrict your possibilities in filtering the data.'|trans }}</p>
{{ form_widget(form.centers.c) }}
{% if form.centers.children.g is defined %}
<h3>{{ 'Pick aggregated centers'|trans }}</h3>
{% for f in form.centers.children.g.children %}
{{ form_row(f) }}
{% endfor %}
<h3 class="m-3">{{ 'Center'|trans }}</h3>
{{ form_widget(form.centers.center) }}
{% if form.centers.regroupment is defined %}
<h3 class="m-3">{{ 'Pick aggregated centers'|trans }}</h3>
{{ form_widget(form.centers.regroupment) }}
{% endif %}
</section>
<p>{{ form_widget(form.submit, { 'attr' : { 'class' : 'btn btn-action btn-create' }, 'label' : 'Go to export options' } ) }}</p>
{{ form_end(form) }}
</div>
{% endblock content %}

View File

@@ -1,15 +1,15 @@
<div class="col-10 mt-5">
{# vue component #}
<div id="homepage_widget"></div>
{% include '@ChillMain/Homepage/fast_actions.html.twig' %}
</div>
{% block css %}
{{ encore_entry_link_tags('page_homepage_widget') }}
{% endblock %}
{% block js %}
{{ encore_entry_script_tags('page_homepage_widget') }}
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends '@ChillMain/Admin/layout.html.twig' %}
{% block title %}
{{ 'absence.My absence'|trans }}
{% endblock title %}
{% block content %}
<div class="col-md-10">
<h2>{{ 'absence.My absence'|trans }}</h2>
{% if user.absenceStart is not null %}
<div>
<p>{{ 'absence.You are listed as absent, as of'|trans }} {{ user.absenceStart|format_date('long') }}</p>
<ul class="record_actions sticky-form-buttons">
<li>
<a href="{{ path('chill_main_user_absence_unset') }}"
class="btn btn-delete">{{ 'absence.Unset absence'|trans }}</a>
</li>
</ul>
</div>
{% else %}
<div>
<p class="chill-no-data-statement">{{ 'absence.No absence listed'|trans }}</p>
</div>
<div>
{{ form_start(form) }}
{{ form_row(form.absenceStart) }}
<ul class="record_actions sticky-form-buttons">
<li>
<button class="btn btn-save" type="submit">
{{ 'Save'|trans }}
</button>
</li>
</ul>
{{ form_end(form) }}
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -2,20 +2,21 @@
{% block admin_content %}
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% block index_header %}
<h1>{{"Users"|trans}}</h1>
{% endblock %}
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
{% block table_entities_thead_tr %}
<th>{{ 'Active'|trans }}</th>
<th>{{ 'absence.Is absent'|trans }}</th>
<th>{{ 'Username'|trans }}</th>
<th>{{ 'Datas'|trans }}</th>
<th>{{ 'Actions'|trans }}</th>
{% endblock %}
{% block table_entities_tbody %}
{% for entity in entities %}
<tr>
@@ -26,6 +27,13 @@
<i class="fa fa-square-o"></i>
{% endif %}
</td>
<td>
{% if entity.isAbsent %}
<i class="fa fa-check-square-o"></i>
{% else %}
<i class="fa fa-square-o"></i>
{% endif %}
</td>
<td>
{#
{% if entity.civility is not null %}
@@ -64,13 +72,13 @@
<li>
<a class="btn btn-edit" title="{{ 'Edit'|trans }}" href="{{ path('chill_crud_admin_user_edit', { 'id': entity.id }) }}"></a>
</li>
{% if allow_change_password is same as(true) %}
<li>
<a class="btn btn-chill-red" href="{{ path('admin_user_edit_password', { 'id' : entity.id }) }}" title="{{ 'Edit password'|trans|e('html_attr') }}"><i class="fa fa-ellipsis-h"></i></a>
</li>
{% endif %}
{% if is_granted('ROLE_ALLOWED_TO_SWITCH') %}
<li>
<a class="btn btn-chill-blue" href="{{ path('chill_main_homepage', {'_switch_user': entity.username }) }}" title="{{ "Impersonate"|trans|e('html_attr') }}"><i class="fa fa-user-secret"></i></a>
@@ -81,9 +89,9 @@
</tr>
{% endfor %}
{% endblock %}
{% block pagination %}{{ chill_pagination(paginator) }}{% endblock %}
{% block list_actions %}
<ul class="record_actions sticky-form-buttons">
<li class='cancel'>
@@ -94,6 +102,6 @@
</li>
</ul>
{% endblock list_actions %}
{% endembed %}
{% endblock %}

View File

@@ -72,7 +72,7 @@
</div>
</div>
<div class="item-row column">
<table class="obj-res-eval my-3">
<table class="obj-res-eval smallfont my-3">
<thead>
<tr><th class="obj"><h4 class="title_label">Objectif - motif - dispositif</h4></th>
<th class="res"><h4 class="title_label">Résultats - orientations</h4></th>

View File

@@ -69,18 +69,26 @@
{% block content %}
<div class="col-8 main_search">
{% if app.user.isAbsent %}
<div class="d-flex flex-row mb-5 alert alert-warning" role="alert">
<p class="m-2">{{'absence.You are marked as being absent'|trans }}</p>
<span class="ms-auto">
<a class="btn btn-remove" title="Modifier" href="{{ path('chill_main_user_absence_index') }}">{{ 'absence.Unset absence'|trans }}</a>
</span>
</div>
{% endif %}
<h2>{{ 'Search'|trans }}</h2>
<form action="{{ path('chill_main_search') }}" method="get">
<input class="form-control form-control-lg" name="q" type="search" placeholder="{{ 'Search persons, ...'|trans }}" />
<center>
<div class="text-center">
<button type="submit" class="btn btn-lg btn-warning mt-3">
<i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }}
</button>
<a class="btn btn-lg btn-misc mt-3" href="{{ path('chill_main_advanced_search_list') }}">
<i class="fa fa-fw fa-search"></i> {{ 'Advanced search'|trans }}
</a>
</center>
</div>
</form>
</div>

View File

@@ -78,6 +78,15 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
$nbNotifications = $this->notificationByUserCounter->countUnreadByUser($user);
//TODO add an icon? How exactly? For example a clock icon...
$menu
->addChild($this->translator->trans('absence.Set absence date'), [
'route' => 'chill_main_user_absence_index',
])
->setExtras([
'order' => -8888888,
]);
$menu
->addChild(
$this->translator->trans('notification.My notifications with counter', ['nb' => $nbNotifications]),

View File

@@ -31,6 +31,7 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
'id' => '',
'username' => '',
'text' => '',
'text_without_absent' => '',
'label' => '',
'email' => '',
];
@@ -82,11 +83,13 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
'id' => $object->getId(),
'username' => $object->getUsername(),
'text' => $this->userRender->renderString($object, []),
'text_without_absent' => $this->userRender->renderString($object, ['absence' => false]),
'label' => $object->getLabel(),
'email' => (string) $object->getEmail(),
'user_job' => $this->normalizer->normalize($object->getUserJob(), $format, $userJobContext),
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
'main_scope' => $this->normalizer->normalize($object->getMainScope(), $format, $scopeContext),
'isAbsent' => $object->isAbsent(),
];
if ('docgen' === $format) {

View File

@@ -13,8 +13,10 @@ namespace Chill\MainBundle\Templating\Entity;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Component\Templating\EngineInterface;
use DateTimeImmutable;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function array_merge;
class UserRender implements ChillEntityRenderInterface
@@ -22,16 +24,20 @@ class UserRender implements ChillEntityRenderInterface
public const DEFAULT_OPTIONS = [
'main_scope' => true,
'user_job' => true,
'absence' => true,
];
private EngineInterface $engine;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(TranslatableStringHelper $translatableStringHelper, EngineInterface $engine)
private TranslatorInterface $translator;
public function __construct(TranslatableStringHelper $translatableStringHelper, EngineInterface $engine, TranslatorInterface $translator)
{
$this->translatableStringHelper = $translatableStringHelper;
$this->engine = $engine;
$this->translator = $translator;
}
public function renderBox($entity, array $options): string
@@ -63,6 +69,10 @@ class UserRender implements ChillEntityRenderInterface
->localize($entity->getMainScope()->getName()) . ')';
}
if ($entity->isAbsent() && $opts['absence']) {
$str .= ' (' . $this->translator->trans('absence.Absent') . ')';
}
return $str;
}

View File

@@ -138,9 +138,9 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Form\RegroupmentType:
autowire: true
autoconfigure: true
Chill\MainBundle\Form\AbsenceType: ~
Chill\MainBundle\Form\DataMapper\RegroupmentDataMapper: ~
Chill\MainBundle\Form\RegroupmentType: ~
Chill\MainBundle\Form\DataTransformer\IdToLocationDataTransformer: ~
Chill\MainBundle\Form\DataTransformer\IdToUserDataTransformer: ~

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230111160610 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users DROP absenceStart');
}
public function getDescription(): string
{
return 'Add absence property to user';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE users ADD absenceStart TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN users.absenceStart IS \'(DC2Type:datetime_immutable)\'');
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20230301155213 extends AbstractMigration
{
public function getDescription(): string
{
return 'Alter type to TEXT for regroupment.name';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE regroupment ALTER name TYPE TEXT');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE regroupment ALTER name TYPE VARCHAR(15)');
}
}

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