mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-04 03:08:25 +00:00 
			
		
		
		
	Compare commits
	
		
			160 Commits
		
	
	
		
			694-save-e
			...
			71-adapt-r
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2dcc65d172 | |||
| 2d6a0d14eb | |||
| fcb057c55b | |||
| 
						
						
							
						
						96ddc73e45
	
				 | 
					
					
						|||
| 
						
						
							
						
						4eb7d10e45
	
				 | 
					
					
						|||
| efa475df0f | |||
| 
						
						
							
						
						a8977729fe
	
				 | 
					
					
						|||
| 
						
						
							
						
						ecac409586
	
				 | 
					
					
						|||
| 
						
						
							
						
						df2480c47c
	
				 | 
					
					
						|||
| c729a14304 | |||
| 06b7e84270 | |||
| 
						
						
							
						
						1cc80c8e6a
	
				 | 
					
					
						|||
| 
						
						
							
						
						c3558beee1
	
				 | 
					
					
						|||
| 
						
						
							
						
						44ecad2bca
	
				 | 
					
					
						|||
| 
						
						
							
						
						d1bdf41c4c
	
				 | 
					
					
						|||
| 4a30f310b8 | |||
| 896b4cdfe3 | |||
| a272dabcaf | |||
| 3901fe2d32 | |||
| 
						
						
							
						
						78858e84f2
	
				 | 
					
					
						|||
| cc98f64be5 | |||
| 
						
						
							
						
						6e812b54e1
	
				 | 
					
					
						|||
| 
						
						
							
						
						83e0a50b57
	
				 | 
					
					
						|||
| 
						
						
							
						
						3aac4d5d35
	
				 | 
					
					
						|||
| 
						
						
							
						
						23cee274a5
	
				 | 
					
					
						|||
| aacb54037b | |||
| 57cb96320c | |||
| 5a2d80cb4d | |||
| 2e822a9486 | |||
| f376b1af49 | |||
| dd621186e8 | |||
| 1b15abe635 | |||
| 1965fc55f4 | |||
| a9290eb3fe | |||
| 
						
						
							
						
						e78eb8789d
	
				 | 
					
					
						|||
| 9911112e08 | |||
| 
						
						
							
						
						c0675aee9b
	
				 | 
					
					
						|||
| 
						
						
							
						
						03ee04978c
	
				 | 
					
					
						|||
| 15d68df8c6 | |||
| 
						
						
							
						
						1b2c0ecc87
	
				 | 
					
					
						|||
| 
						
						
							
						
						f15017ebd7
	
				 | 
					
					
						|||
| 
						
						
							
						
						9a56a1b115
	
				 | 
					
					
						|||
| 
						
						
							
						
						11e7f2179c
	
				 | 
					
					
						|||
| 
						
						
							
						
						e982e81900
	
				 | 
					
					
						|||
| 
						
						
							
						
						e50b02a8c7
	
				 | 
					
					
						|||
| 
						
						
							
						
						bea839663f
	
				 | 
					
					
						|||
| 
						
						
							
						
						ac4c821290
	
				 | 
					
					
						|||
| c953da3fd0 | |||
| 
						
						
							
						
						f5d17eb38c
	
				 | 
					
					
						|||
| 73f332927d | |||
| ef75deda26 | |||
| 6264a95d62 | |||
| 26a6169b95 | |||
| 678defdee7 | |||
| 88ccbd450a | |||
| 
						
						
							
						
						62532e0a90
	
				 | 
					
					
						|||
| 
						
						
							
						
						73fa585707
	
				 | 
					
					
						|||
| 21a16dcbe2 | |||
| 
						
						
							
						
						b30e966316
	
				 | 
					
					
						|||
| 
						
						
							
						
						9696a8194c
	
				 | 
					
					
						|||
| 
						
						
							
						
						6749758b46
	
				 | 
					
					
						|||
| 
						
						
							
						
						f1ebc089c3
	
				 | 
					
					
						|||
| 813adc70f4 | |||
| 95984eff6d | |||
| 
						
						
							
						
						3db5b62d57
	
				 | 
					
					
						|||
| 
						
						
							
						
						cf1cc937ca
	
				 | 
					
					
						|||
| 
						
						
							
						
						14df8fe9ad
	
				 | 
					
					
						|||
| 
						
						
							
						
						fe4388c884
	
				 | 
					
					
						|||
| 
						
						
							
						
						dbcc425f5f
	
				 | 
					
					
						|||
| 
						
						
							
						
						77c545344c
	
				 | 
					
					
						|||
| c13d672db2 | |||
| a16244a3f5 | |||
| 27f13e0dd1 | |||
| f07ea3259e | |||
| 1f4438690e | |||
| 744b62184a | |||
| 5ee0ab5ab8 | |||
| 4370349f10 | |||
| 
						
						
							
						
						6254303392
	
				 | 
					
					
						|||
| 
						
						
							
						
						9676975cd8
	
				 | 
					
					
						|||
| 
						
						
							
						
						7c4bc8f46a
	
				 | 
					
					
						|||
| 
						
						
							
						
						55a845fcd6
	
				 | 
					
					
						|||
| 
						
						
							
						
						91d21ba939
	
				 | 
					
					
						|||
| 
						
						
							
						
						55918bcafb
	
				 | 
					
					
						|||
| 
						
						
							
						
						bb05ba0f17
	
				 | 
					
					
						|||
| eac3471cbb | |||
| f653f8fd7a | |||
| 6d2c6fb6e1 | |||
| 
						
						
							
						
						aea6272c4d
	
				 | 
					
					
						|||
| 
						
						
							
						
						25cbb528ec
	
				 | 
					
					
						|||
| 
						
						
							
						
						2d013e110a
	
				 | 
					
					
						|||
| d6df16973a | |||
| 80835dd7c3 | |||
| 
						
						
							
						
						9e63480c70
	
				 | 
					
					
						|||
| 988495df27 | |||
| cc62c9cc4a | |||
| 40924d9d39 | |||
| 8b505410ca | |||
| d535ec6cfb | |||
| 9029426d03 | |||
| 9ae2e51819 | |||
| 68f7a832b4 | |||
| af5f27ff49 | |||
| 5e58d36e79 | |||
| 
						
						
							
						
						b0ab591cbd
	
				 | 
					
					
						|||
| 
						
						
							
						
						d8af7d455e
	
				 | 
					
					
						|||
| 
						
						
							
						
						5830c3e177
	
				 | 
					
					
						|||
| 1285100801 | |||
| a4e21b7834 | |||
| 1a44a516c2 | |||
| fb9b9b9226 | |||
| 0ace1c1f6a | |||
| 7d80507517 | |||
| b5ec0919e7 | |||
| 068311d071 | |||
| 
						
						
							
						
						88eefa698b
	
				 | 
					
					
						|||
| 5756a37178 | |||
| c83e8ad9a4 | |||
| c64ec89274 | |||
| 16ec858ee8 | |||
| 
						
						
							
						
						de55ff920f
	
				 | 
					
					
						|||
| 
						
						
							
						
						885256ac0d
	
				 | 
					
					
						|||
| f53d3852c3 | |||
| 9f5b11e6cc | |||
| e5bc74d11d | |||
| 5c0d89a88b | |||
| 56a17a0bcd | |||
| 5bfd2aefe6 | |||
| 2165e04ec3 | |||
| 1a8e21a77f | |||
| 9ffe1ff8a8 | |||
| 50bb8f10cf | |||
| 1c673db628 | |||
| fa8a2c5cc5 | |||
| d46304e229 | |||
| 4dd81da1ef | |||
| 882e72b609 | |||
| bb7d072cc8 | |||
| f76c031ff3 | |||
| 86b5f4dfac | |||
| 
						
						
							
						
						ded71c5997
	
				 | 
					
					
						|||
| 
						
						
							
						
						5ae4eb1bf7
	
				 | 
					
					
						|||
| c790b22496 | |||
| e54c2ca712 | |||
| 2f091a639b | |||
| 7f9e045d5d | |||
| 9ada19ef23 | |||
| 8ee184e665 | |||
| c5f842076f | |||
| 8e3a83de85 | |||
| 050a4feab5 | |||
| de9d53936f | |||
| 6c1108b8aa | |||
| 5bbe5af124 | |||
| 2c5c815f68 | |||
| 44ef21f940 | |||
| 68998c9156 | |||
| b93b78615b | |||
| b2924ede70 | |||
| fb51e44e45 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -5,6 +5,7 @@ composer.lock
 | 
			
		||||
docs/build/
 | 
			
		||||
node_modules/*
 | 
			
		||||
.php_cs.cache
 | 
			
		||||
.cache/*
 | 
			
		||||
 | 
			
		||||
###> symfony/framework-bundle ###
 | 
			
		||||
/.env.local
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,74 @@
 | 
			
		||||
<?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\ActivityBundle\Export\Filter\ACPFilters;
 | 
			
		||||
 | 
			
		||||
use Chill\ActivityBundle\Export\Declarations;
 | 
			
		||||
use Chill\MainBundle\Export\FilterInterface;
 | 
			
		||||
use Chill\MainBundle\Form\Type\PickUserLocationType;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class LocationFilter implements FilterInterface
 | 
			
		||||
{
 | 
			
		||||
    private TranslatableStringHelper $translatableStringHelper;
 | 
			
		||||
 | 
			
		||||
    public function __construct(TranslatableStringHelper $translatableStringHelper)
 | 
			
		||||
    {
 | 
			
		||||
        $this->translatableStringHelper = $translatableStringHelper;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addRole(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function alterQuery(QueryBuilder $qb, $data)
 | 
			
		||||
    {
 | 
			
		||||
        $qb->andWhere(
 | 
			
		||||
            $qb->expr()->in('activity.location', ':location')
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $qb->setParameter('location', $data['accepted_location']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function applyOn(): string
 | 
			
		||||
    {
 | 
			
		||||
        return Declarations::ACTIVITY_ACP;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder)
 | 
			
		||||
    {
 | 
			
		||||
        $builder->add('accepted_location', PickUserLocationType::class, [
 | 
			
		||||
            'multiple' => true,
 | 
			
		||||
            'label' => 'pick location',
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function describeAction($data, $format = 'string'): array
 | 
			
		||||
    {
 | 
			
		||||
        $locations = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($data['accepted_location'] as $location) {
 | 
			
		||||
            $locations[] = $location->getName();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ['Filtered activity by location: only %locations%', [
 | 
			
		||||
            '%locations%' => implode(', ', $locations),
 | 
			
		||||
        ]];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Filter activity by location';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 }}{{ m.download_button(d) }}</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 %}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,14 @@
 | 
			
		||||
    {{ parent() }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
 | 
			
		||||
    {{ 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_notification_toggle_read_status') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_async_upload') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_document_action_buttons_group') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,13 +7,13 @@
 | 
			
		||||
{% block js %}
 | 
			
		||||
    {{ parent() }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_async_upload') }}
 | 
			
		||||
    {{ 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_async_upload') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_document_action_buttons_group') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,11 @@ services:
 | 
			
		||||
        tags:
 | 
			
		||||
            - { name: chill.export_filter, alias: 'accompanyingcourse_activitytype_filter' }
 | 
			
		||||
 | 
			
		||||
    chill.activity.export.location_filter:
 | 
			
		||||
        class: Chill\ActivityBundle\Export\Filter\ACPFilters\LocationFilter
 | 
			
		||||
        tags:
 | 
			
		||||
            - { name: chill.export_filter, alias: 'activity_location_filter' }
 | 
			
		||||
 | 
			
		||||
    chill.activity.export.locationtype_filter:
 | 
			
		||||
        class: Chill\ActivityBundle\Export\Filter\ACPFilters\LocationTypeFilter
 | 
			
		||||
        tags:
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -252,6 +252,8 @@ Activity reasons for those activities: Sujets de ces activités
 | 
			
		||||
 | 
			
		||||
Filter by activity type: Filtrer les activités par type
 | 
			
		||||
 | 
			
		||||
Filter activity by location: Filtrer les activités par localisation
 | 
			
		||||
'Filtered activity by location: only %locations%': "Filtré par localisation: uniquement %locations%"
 | 
			
		||||
Filter activity by locationtype: Filtrer les activités par type de localisation
 | 
			
		||||
'Filtered activity by locationtype: only %types%': "Filtré par type de localisation: uniquement %types%"
 | 
			
		||||
Accepted locationtype: Types de localisation
 | 
			
		||||
 
 | 
			
		||||
@@ -53,19 +53,15 @@ class ByActivityTypeAggregator implements AggregatorInterface
 | 
			
		||||
 | 
			
		||||
    public function getLabels($key, array $values, $data)
 | 
			
		||||
    {
 | 
			
		||||
        $this->asideActivityCategoryRepository->findBy(['id' => $values]);
 | 
			
		||||
 | 
			
		||||
        return function ($value): string {
 | 
			
		||||
            if ('_header' === $value) {
 | 
			
		||||
                return 'export.aggregator.Aside activity type';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (null === $value) {
 | 
			
		||||
            if (null === $value || null === $t = $this->asideActivityCategoryRepository->find($value)) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $t = $this->asideActivityCategoryRepository->find($value);
 | 
			
		||||
 | 
			
		||||
            return $this->translatableStringHelper->localize($t->getTitle());
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,89 @@
 | 
			
		||||
<?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\Aggregator;
 | 
			
		||||
 | 
			
		||||
use Chill\AsideActivityBundle\Export\Declarations;
 | 
			
		||||
use Chill\MainBundle\Export\AggregatorInterface;
 | 
			
		||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
use function in_array;
 | 
			
		||||
 | 
			
		||||
class ByUserJobAggregator implements AggregatorInterface
 | 
			
		||||
{
 | 
			
		||||
    private TranslatableStringHelperInterface $translatableStringHelper;
 | 
			
		||||
 | 
			
		||||
    private UserJobRepositoryInterface $userJobRepository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(UserJobRepositoryInterface $userJobRepository, TranslatableStringHelperInterface $translatableStringHelper)
 | 
			
		||||
    {
 | 
			
		||||
        $this->userJobRepository = $userJobRepository;
 | 
			
		||||
        $this->translatableStringHelper = $translatableStringHelper;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addRole(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function alterQuery(QueryBuilder $qb, $data)
 | 
			
		||||
    {
 | 
			
		||||
        if (!in_array('aside_user', $qb->getAllAliases(), true)) {
 | 
			
		||||
            $qb->leftJoin('aside.agent', 'aside_user');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $qb
 | 
			
		||||
            ->addSelect('IDENTITY(aside_user.userJob) AS aside_activity_user_job_aggregator')
 | 
			
		||||
            ->addGroupBy('aside_activity_user_job_aggregator');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function applyOn()
 | 
			
		||||
    {
 | 
			
		||||
        return Declarations::ASIDE_ACTIVITY_TYPE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder)
 | 
			
		||||
    {
 | 
			
		||||
        // nothing to add in the form
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabels($key, array $values, $data)
 | 
			
		||||
    {
 | 
			
		||||
        return function ($value): string {
 | 
			
		||||
            if ('_header' === $value) {
 | 
			
		||||
                return 'Users \'s job';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (null === $value || '' === $value) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $j = $this->userJobRepository->find($value);
 | 
			
		||||
 | 
			
		||||
            return $this->translatableStringHelper->localize(
 | 
			
		||||
                $j->getLabel()
 | 
			
		||||
            );
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getQueryKeys($data): array
 | 
			
		||||
    {
 | 
			
		||||
        return ['aside_activity_user_job_aggregator'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle()
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.aggregator.Aggregate by user job';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,89 @@
 | 
			
		||||
<?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\Aggregator;
 | 
			
		||||
 | 
			
		||||
use Chill\AsideActivityBundle\Export\Declarations;
 | 
			
		||||
use Chill\MainBundle\Export\AggregatorInterface;
 | 
			
		||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
use function in_array;
 | 
			
		||||
 | 
			
		||||
class ByUserScopeAggregator implements AggregatorInterface
 | 
			
		||||
{
 | 
			
		||||
    private ScopeRepositoryInterface $scopeRepository;
 | 
			
		||||
 | 
			
		||||
    private TranslatableStringHelperInterface $translatableStringHelper;
 | 
			
		||||
 | 
			
		||||
    public function __construct(ScopeRepositoryInterface $scopeRepository, TranslatableStringHelperInterface $translatableStringHelper)
 | 
			
		||||
    {
 | 
			
		||||
        $this->scopeRepository = $scopeRepository;
 | 
			
		||||
        $this->translatableStringHelper = $translatableStringHelper;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addRole(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function alterQuery(QueryBuilder $qb, $data)
 | 
			
		||||
    {
 | 
			
		||||
        if (!in_array('aside_user', $qb->getAllAliases(), true)) {
 | 
			
		||||
            $qb->leftJoin('aside.agent', 'aside_user');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $qb
 | 
			
		||||
            ->addSelect('IDENTITY(aside_user.mainScope) AS aside_activity_user_scope_aggregator')
 | 
			
		||||
            ->addGroupBy('aside_activity_user_scope_aggregator');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function applyOn()
 | 
			
		||||
    {
 | 
			
		||||
        return Declarations::ASIDE_ACTIVITY_TYPE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder)
 | 
			
		||||
    {
 | 
			
		||||
        // nothing to add in the form
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabels($key, array $values, $data)
 | 
			
		||||
    {
 | 
			
		||||
        return function ($value): string {
 | 
			
		||||
            if ('_header' === $value) {
 | 
			
		||||
                return 'Users \'s scope';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (null === $value || '' === $value) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $s = $this->scopeRepository->find($value);
 | 
			
		||||
 | 
			
		||||
            return $this->translatableStringHelper->localize(
 | 
			
		||||
                $s->getName()
 | 
			
		||||
            );
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getQueryKeys($data): array
 | 
			
		||||
    {
 | 
			
		||||
        return ['aside_activity_user_scope_aggregator'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle()
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.aggregator.Aggregate by user scope';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,102 @@
 | 
			
		||||
<?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\Export\Declarations;
 | 
			
		||||
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
 | 
			
		||||
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
 | 
			
		||||
use Chill\MainBundle\Export\ExportInterface;
 | 
			
		||||
use Chill\MainBundle\Export\FormatterInterface;
 | 
			
		||||
use Chill\MainBundle\Export\GroupedExportInterface;
 | 
			
		||||
use Doctrine\ORM\Query;
 | 
			
		||||
use LogicException;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class AvgAsideActivityDuration implements ExportInterface, GroupedExportInterface
 | 
			
		||||
{
 | 
			
		||||
    private AsideActivityRepository $repository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        AsideActivityRepository $repository
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->repository = $repository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAllowedFormattersTypes(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [FormatterInterface::TYPE_TABULAR];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.Average aside activities duration';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getGroup(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.Exports of aside activities';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabels($key, array $values, $data)
 | 
			
		||||
    {
 | 
			
		||||
        if ('export_avg_aside_activity_duration' !== $key) {
 | 
			
		||||
            throw new LogicException("the key {$key} is not used by this export");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return static fn ($value) => '_header' === $value ? 'Average duration aside activities' : $value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getQueryKeys($data): array
 | 
			
		||||
    {
 | 
			
		||||
        return ['export_avg_aside_activity_duration'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getResult($query, $data)
 | 
			
		||||
    {
 | 
			
		||||
        return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.Average aside activities duration';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getType(): string
 | 
			
		||||
    {
 | 
			
		||||
        return Declarations::ASIDE_ACTIVITY_TYPE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->repository->createQueryBuilder('aside');
 | 
			
		||||
 | 
			
		||||
        $qb
 | 
			
		||||
            ->select('AVG(aside.duration) as export_avg_aside_activity_duration')
 | 
			
		||||
            ->andWhere($qb->expr()->isNotNull('aside.duration'));
 | 
			
		||||
 | 
			
		||||
        return $qb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function requiredRole(): string
 | 
			
		||||
    {
 | 
			
		||||
        return AsideActivityVoter::STATS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsModifiers(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [Declarations::ASIDE_ACTIVITY_TYPE];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,12 +11,12 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\AsideActivityBundle\Export\Export;
 | 
			
		||||
 | 
			
		||||
use Chill\AsideActivityBundle\Export\Declarations;
 | 
			
		||||
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
 | 
			
		||||
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
 | 
			
		||||
use Chill\MainBundle\Export\ExportInterface;
 | 
			
		||||
use Chill\MainBundle\Export\FormatterInterface;
 | 
			
		||||
use Chill\MainBundle\Export\GroupedExportInterface;
 | 
			
		||||
use ChillAsideActivityBundle\Export\Declarations;
 | 
			
		||||
use Doctrine\ORM\Query;
 | 
			
		||||
use LogicException;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
@@ -100,6 +100,8 @@ class CountAsideActivity implements ExportInterface, GroupedExportInterface
 | 
			
		||||
 | 
			
		||||
    public function supportsModifiers(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
        return [
 | 
			
		||||
            Declarations::ASIDE_ACTIVITY_TYPE,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,243 @@
 | 
			
		||||
<?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\Repository\AsideActivityCategoryRepository;
 | 
			
		||||
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
 | 
			
		||||
use Chill\AsideActivityBundle\Templating\Entity\CategoryRender;
 | 
			
		||||
use Chill\MainBundle\Entity\Center;
 | 
			
		||||
use Chill\MainBundle\Export\FormatterInterface;
 | 
			
		||||
use Chill\MainBundle\Export\GroupedExportInterface;
 | 
			
		||||
use Chill\MainBundle\Export\Helper\DateTimeHelper;
 | 
			
		||||
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\TranslatableStringHelperInterface;
 | 
			
		||||
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 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,
 | 
			
		||||
        UserHelper $userHelper,
 | 
			
		||||
        ScopeRepositoryInterface $scopeRepository,
 | 
			
		||||
        CenterRepositoryInterface $centerRepository,
 | 
			
		||||
        AsideActivityCategoryRepository $asideActivityCategoryRepository,
 | 
			
		||||
        CategoryRender $categoryRender,
 | 
			
		||||
        TranslatableStringHelperInterface $translatableStringHelper
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->em = $em;
 | 
			
		||||
        $this->dateTimeHelper = $dateTimeHelper;
 | 
			
		||||
        $this->userHelper = $userHelper;
 | 
			
		||||
        $this->scopeRepository = $scopeRepository;
 | 
			
		||||
        $this->centerRepository = $centerRepository;
 | 
			
		||||
        $this->asideActivityCategoryRepository = $asideActivityCategoryRepository;
 | 
			
		||||
        $this->categoryRender = $categoryRender;
 | 
			
		||||
        $this->translatableStringHelper = $translatableStringHelper;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAllowedFormattersTypes()
 | 
			
		||||
    {
 | 
			
		||||
        return [FormatterInterface::TYPE_LIST];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getDescription()
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.aside_activity.List of aside activities';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getGroup(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.Exports of aside activities';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabels($key, array $values, $data)
 | 
			
		||||
    {
 | 
			
		||||
        switch ($key) {
 | 
			
		||||
            case 'id':
 | 
			
		||||
            case 'note':
 | 
			
		||||
                return static function ($value) use ($key) {
 | 
			
		||||
                    if ('_header' === $value) {
 | 
			
		||||
                        return 'export.aside_activity.' . $key;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return $value ?? '';
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            case 'duration':
 | 
			
		||||
                return static function ($value) use ($key) {
 | 
			
		||||
                    if ('_header' === $value) {
 | 
			
		||||
                        return 'export.aside_activity.' . $key;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (null === $value) {
 | 
			
		||||
                        return '';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if ($value instanceof DateTimeInterface) {
 | 
			
		||||
                        return $value->format('H:i:s');
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return $value;
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            case 'createdAt':
 | 
			
		||||
            case 'updatedAt':
 | 
			
		||||
            case 'date':
 | 
			
		||||
                return $this->dateTimeHelper->getLabel('export.aside_activity.' . $key);
 | 
			
		||||
 | 
			
		||||
            case 'agent_id':
 | 
			
		||||
            case 'creator_id':
 | 
			
		||||
                return $this->userHelper->getLabel($key, $values, 'export.aside_activity.' . $key);
 | 
			
		||||
 | 
			
		||||
            case 'aside_activity_type':
 | 
			
		||||
                return function ($value) {
 | 
			
		||||
                    if ('_header' === $value) {
 | 
			
		||||
                        return 'export.aside_activity.aside_activity_type';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (null === $value || '' === $value || null === $c = $this->asideActivityCategoryRepository->find($value)) {
 | 
			
		||||
                        return '';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return $this->categoryRender->renderString($c, []);
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            case 'main_scope':
 | 
			
		||||
                return function ($value) {
 | 
			
		||||
                    if ('_header' === $value) {
 | 
			
		||||
                        return 'export.aside_activity.main_scope';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (null === $value || '' === $value || null === $c = $this->scopeRepository->find($value)) {
 | 
			
		||||
                        return '';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return $this->translatableStringHelper->localize($c->getName());
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            case 'main_center':
 | 
			
		||||
                return function ($value) {
 | 
			
		||||
                    if ('_header' === $value) {
 | 
			
		||||
                        return 'export.aside_activity.main_center';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    /** @var Center $c */
 | 
			
		||||
                    if (null === $value || '' === $value || null === $c = $this->centerRepository->find($value)) {
 | 
			
		||||
                        return '';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return $c->getName();
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                throw new LogicException('this key is not supported : ' . $key);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getQueryKeys($data)
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'id',
 | 
			
		||||
            'createdAt',
 | 
			
		||||
            'updatedAt',
 | 
			
		||||
            'agent_id',
 | 
			
		||||
            'creator_id',
 | 
			
		||||
            'main_scope',
 | 
			
		||||
            'main_center',
 | 
			
		||||
            'aside_activity_type',
 | 
			
		||||
            'date',
 | 
			
		||||
            'duration',
 | 
			
		||||
            'note',
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param QueryBuilder $query
 | 
			
		||||
     * @param array $data
 | 
			
		||||
     */
 | 
			
		||||
    public function getResult($query, $data): array
 | 
			
		||||
    {
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->em->createQueryBuilder()
 | 
			
		||||
            ->from(AsideActivity::class, 'aside')
 | 
			
		||||
            ->leftJoin('aside.agent', 'agent');
 | 
			
		||||
 | 
			
		||||
        $qb
 | 
			
		||||
            ->addSelect('aside.id AS id')
 | 
			
		||||
            ->addSelect('aside.createdAt AS createdAt')
 | 
			
		||||
            ->addSelect('aside.updatedAt AS updatedAt')
 | 
			
		||||
            ->addSelect('IDENTITY(aside.agent) AS agent_id')
 | 
			
		||||
            ->addSelect('IDENTITY(aside.createdBy) AS creator_id')
 | 
			
		||||
            ->addSelect('IDENTITY(agent.mainScope) AS main_scope')
 | 
			
		||||
            ->addSelect('IDENTITY(agent.mainCenter) AS main_center')
 | 
			
		||||
            ->addSelect('IDENTITY(aside.type) AS aside_activity_type')
 | 
			
		||||
            ->addSelect('aside.date')
 | 
			
		||||
            ->addSelect('aside.duration')
 | 
			
		||||
            ->addSelect('aside.note');
 | 
			
		||||
 | 
			
		||||
        return $qb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function requiredRole(): string
 | 
			
		||||
    {
 | 
			
		||||
        return AsideActivityVoter::STATS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsModifiers()
 | 
			
		||||
    {
 | 
			
		||||
        return [Declarations::ASIDE_ACTIVITY_TYPE];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,102 @@
 | 
			
		||||
<?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\Export\Declarations;
 | 
			
		||||
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
 | 
			
		||||
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
 | 
			
		||||
use Chill\MainBundle\Export\ExportInterface;
 | 
			
		||||
use Chill\MainBundle\Export\FormatterInterface;
 | 
			
		||||
use Chill\MainBundle\Export\GroupedExportInterface;
 | 
			
		||||
use Doctrine\ORM\Query;
 | 
			
		||||
use LogicException;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class SumAsideActivityDuration implements ExportInterface, GroupedExportInterface
 | 
			
		||||
{
 | 
			
		||||
    private AsideActivityRepository $repository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        AsideActivityRepository $repository
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->repository = $repository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAllowedFormattersTypes(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [FormatterInterface::TYPE_TABULAR];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.Sum aside activities duration';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getGroup(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.Exports of aside activities';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabels($key, array $values, $data)
 | 
			
		||||
    {
 | 
			
		||||
        if ('export_sum_aside_activity_duration' !== $key) {
 | 
			
		||||
            throw new LogicException("the key {$key} is not used by this export");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return static fn ($value) => '_header' === $value ? 'Sum duration aside activities' : $value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getQueryKeys($data): array
 | 
			
		||||
    {
 | 
			
		||||
        return ['export_sum_aside_activity_duration'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getResult($query, $data)
 | 
			
		||||
    {
 | 
			
		||||
        return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.Sum aside activities duration';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getType(): string
 | 
			
		||||
    {
 | 
			
		||||
        return Declarations::ASIDE_ACTIVITY_TYPE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->repository
 | 
			
		||||
            ->createQueryBuilder('aside');
 | 
			
		||||
 | 
			
		||||
        $qb->select('SUM(aside.duration) as export_sum_aside_activity_duration')
 | 
			
		||||
            ->andWhere($qb->expr()->isNotNull('aside.duration'));
 | 
			
		||||
 | 
			
		||||
        return $qb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function requiredRole(): string
 | 
			
		||||
    {
 | 
			
		||||
        return AsideActivityVoter::STATS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsModifiers(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [Declarations::ASIDE_ACTIVITY_TYPE];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -80,8 +80,8 @@ class ByActivityTypeFilter implements FilterInterface
 | 
			
		||||
    public function describeAction($data, $format = 'string'): array
 | 
			
		||||
    {
 | 
			
		||||
        $types = array_map(
 | 
			
		||||
            fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getName()),
 | 
			
		||||
            $this->asideActivityTypeRepository->findBy(['id' => $data['types']->toArray()])
 | 
			
		||||
            fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getTitle()),
 | 
			
		||||
            $data['types']->toArray()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return ['export.filter.Filtered by aside activity type: only %type%', [
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -46,25 +45,18 @@ class ByDateFilter implements FilterInterface
 | 
			
		||||
 | 
			
		||||
    public function alterQuery(QueryBuilder $qb, $data)
 | 
			
		||||
    {
 | 
			
		||||
        $where = $qb->getDQLPart('where');
 | 
			
		||||
        $clause = $qb->expr()->between(
 | 
			
		||||
            'aside.date',
 | 
			
		||||
            ':date_from',
 | 
			
		||||
            ':date_to'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if ($where instanceof Andx) {
 | 
			
		||||
            $where->add($clause);
 | 
			
		||||
        } else {
 | 
			
		||||
            $where = $qb->expr()->andX($clause);
 | 
			
		||||
        }
 | 
			
		||||
        $qb->andWhere($clause);
 | 
			
		||||
 | 
			
		||||
        $qb->add('where', $where);
 | 
			
		||||
        $qb->setParameter(
 | 
			
		||||
            'date_from',
 | 
			
		||||
            $this->rollingDateConverter->convert($data['date_from'])
 | 
			
		||||
        );
 | 
			
		||||
        $qb->setParameter(
 | 
			
		||||
        )->setParameter(
 | 
			
		||||
            'date_to',
 | 
			
		||||
            $this->rollingDateConverter->convert($data['date_to'])
 | 
			
		||||
        );
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,74 @@
 | 
			
		||||
<?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\Filter;
 | 
			
		||||
 | 
			
		||||
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\QueryBuilder;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class ByUserFilter implements FilterInterface
 | 
			
		||||
{
 | 
			
		||||
    private UserRender $userRender;
 | 
			
		||||
 | 
			
		||||
    public function __construct(UserRender $userRender)
 | 
			
		||||
    {
 | 
			
		||||
        $this->userRender = $userRender;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addRole(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function alterQuery(QueryBuilder $qb, $data)
 | 
			
		||||
    {
 | 
			
		||||
        $clause = $qb->expr()->in('aside.agent', ':users');
 | 
			
		||||
 | 
			
		||||
        $qb
 | 
			
		||||
            ->andWhere($clause)
 | 
			
		||||
            ->setParameter('users', $data['accepted_users']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function applyOn(): string
 | 
			
		||||
    {
 | 
			
		||||
        return Declarations::ASIDE_ACTIVITY_TYPE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder)
 | 
			
		||||
    {
 | 
			
		||||
        $builder->add('accepted_users', PickUserDynamicType::class, [
 | 
			
		||||
            'multiple' => true,
 | 
			
		||||
            'label' => 'Creators',
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function describeAction($data, $format = 'string'): array
 | 
			
		||||
    {
 | 
			
		||||
        $users = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($data['accepted_users'] as $u) {
 | 
			
		||||
            $users[] = $this->userRender->renderString($u, []);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ['export.filter.Filtered aside activity by user: only %users%', [
 | 
			
		||||
            '%users%' => implode(', ', $users),
 | 
			
		||||
        ]];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.filter.Filter aside activity by user';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,81 @@
 | 
			
		||||
<?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\Filter;
 | 
			
		||||
 | 
			
		||||
use Chill\AsideActivityBundle\Entity\AsideActivity;
 | 
			
		||||
use Chill\AsideActivityBundle\Export\Declarations;
 | 
			
		||||
use Chill\MainBundle\Entity\UserJob;
 | 
			
		||||
use Chill\MainBundle\Export\FilterInterface;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class ByUserJobFilter implements FilterInterface
 | 
			
		||||
{
 | 
			
		||||
    private TranslatableStringHelperInterface $translatableStringHelper;
 | 
			
		||||
 | 
			
		||||
    public function __construct(TranslatableStringHelperInterface $translatableStringHelper)
 | 
			
		||||
    {
 | 
			
		||||
        $this->translatableStringHelper = $translatableStringHelper;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addRole(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function alterQuery(QueryBuilder $qb, $data)
 | 
			
		||||
    {
 | 
			
		||||
        $qb
 | 
			
		||||
            ->andWhere(
 | 
			
		||||
                $qb->expr()->exists(
 | 
			
		||||
                    'SELECT 1 FROM ' . AsideActivity::class . ' aside_activity_user_job_filter_act
 | 
			
		||||
                JOIN aside_activity_user_job_filter_act.agent aside_activity_user_job_filter_user WHERE aside_activity_user_job_filter_user.userJob IN (:aside_activity_user_job_filter_jobs) AND aside_activity_user_job_filter_act = aside'
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            ->setParameter('aside_activity_user_job_filter_jobs', $data['jobs']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function applyOn()
 | 
			
		||||
    {
 | 
			
		||||
        return Declarations::ASIDE_ACTIVITY_TYPE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder)
 | 
			
		||||
    {
 | 
			
		||||
        $builder->add('jobs', EntityType::class, [
 | 
			
		||||
            'class' => UserJob::class,
 | 
			
		||||
            'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
 | 
			
		||||
            'multiple' => true,
 | 
			
		||||
            'expanded' => true,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function describeAction($data, $format = 'string')
 | 
			
		||||
    {
 | 
			
		||||
        return ['export.filter.Filtered aside activities by user jobs: only %jobs%', [
 | 
			
		||||
            '%jobs%' => implode(
 | 
			
		||||
                ', ',
 | 
			
		||||
                array_map(
 | 
			
		||||
                    fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
 | 
			
		||||
                    $data['jobs']->toArray()
 | 
			
		||||
                )
 | 
			
		||||
            ),
 | 
			
		||||
        ]];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle()
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.filter.Filter by user jobs';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,88 @@
 | 
			
		||||
<?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\Filter;
 | 
			
		||||
 | 
			
		||||
use Chill\AsideActivityBundle\Entity\AsideActivity;
 | 
			
		||||
use Chill\AsideActivityBundle\Export\Declarations;
 | 
			
		||||
use Chill\MainBundle\Entity\Scope;
 | 
			
		||||
use Chill\MainBundle\Export\FilterInterface;
 | 
			
		||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class ByUserScopeFilter implements FilterInterface
 | 
			
		||||
{
 | 
			
		||||
    private ScopeRepositoryInterface $scopeRepository;
 | 
			
		||||
 | 
			
		||||
    private TranslatableStringHelperInterface $translatableStringHelper;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        ScopeRepositoryInterface $scopeRepository,
 | 
			
		||||
        TranslatableStringHelperInterface $translatableStringHelper
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->scopeRepository = $scopeRepository;
 | 
			
		||||
        $this->translatableStringHelper = $translatableStringHelper;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addRole(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function alterQuery(QueryBuilder $qb, $data)
 | 
			
		||||
    {
 | 
			
		||||
        $qb
 | 
			
		||||
            ->andWhere(
 | 
			
		||||
                $qb->expr()->exists(
 | 
			
		||||
                    'SELECT 1 FROM ' . AsideActivity::class . ' aside_activity_user_scope_filter_act
 | 
			
		||||
                JOIN aside_activity_user_scope_filter_act.agent aside_activity_user_scope_filter_user WHERE aside_activity_user_scope_filter_user.mainScope IN (:aside_activity_user_scope_filter_scopes) AND  aside_activity_user_scope_filter_act = aside '
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            ->setParameter('aside_activity_user_scope_filter_scopes', $data['scopes']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function applyOn()
 | 
			
		||||
    {
 | 
			
		||||
        return Declarations::ASIDE_ACTIVITY_TYPE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder)
 | 
			
		||||
    {
 | 
			
		||||
        $builder->add('scopes', EntityType::class, [
 | 
			
		||||
            'class' => Scope::class,
 | 
			
		||||
            'choices' => $this->scopeRepository->findAllActive(),
 | 
			
		||||
            'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
 | 
			
		||||
            'multiple' => true,
 | 
			
		||||
            'expanded' => true,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function describeAction($data, $format = 'string')
 | 
			
		||||
    {
 | 
			
		||||
        return ['export.filter.Filtered aside activities by user scope: only %scopes%', [
 | 
			
		||||
            '%scopes%' => implode(
 | 
			
		||||
                ', ',
 | 
			
		||||
                array_map(
 | 
			
		||||
                    fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
 | 
			
		||||
                    $data['scopes']->toArray()
 | 
			
		||||
                )
 | 
			
		||||
            ),
 | 
			
		||||
        ]];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle()
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.filter.Filter by user scope';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -20,33 +20,3 @@ services:
 | 
			
		||||
    resource: "../Controller"
 | 
			
		||||
    autowire: true
 | 
			
		||||
    autoconfigure: true
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  ## Exports
 | 
			
		||||
 | 
			
		||||
  # indicators
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Export\CountAsideActivity:
 | 
			
		||||
    autowire: true
 | 
			
		||||
    autoconfigure: true
 | 
			
		||||
    tags:
 | 
			
		||||
      - { name: chill.export, alias: count_asideactivity }
 | 
			
		||||
 | 
			
		||||
  # filters
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Filter\ByDateFilter:
 | 
			
		||||
    autowire: true
 | 
			
		||||
    autoconfigure: true
 | 
			
		||||
    tags:
 | 
			
		||||
      - { name: chill.export_filter, alias: asideactivity_bydate_filter }
 | 
			
		||||
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Filter\ByActivityTypeFilter:
 | 
			
		||||
    autowire: true
 | 
			
		||||
    autoconfigure: true
 | 
			
		||||
    tags:
 | 
			
		||||
      - { name: chill.export_filter, alias: asideactivity_activitytype_filter }
 | 
			
		||||
 | 
			
		||||
  # aggregators
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator:
 | 
			
		||||
    autowire: true
 | 
			
		||||
    autoconfigure: true
 | 
			
		||||
    tags:
 | 
			
		||||
      - { name: chill.export_aggregator, alias: asideactivity_activitytype_aggregator }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,23 @@ services:
 | 
			
		||||
    autowire: true
 | 
			
		||||
    autoconfigure: true
 | 
			
		||||
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Export\ListAsideActivity:
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export, alias: 'list_aside_activity' }
 | 
			
		||||
 | 
			
		||||
  ## Indicators
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Export\CountAsideActivity:
 | 
			
		||||
    tags:
 | 
			
		||||
      - { name: chill.export, alias: 'count_aside_activity' }
 | 
			
		||||
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Export\SumAsideActivityDuration:
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export, alias: 'sum_aside_activity_duration' }
 | 
			
		||||
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Export\AvgAsideActivityDuration:
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export, alias: 'avg_aside_activity_duration' }
 | 
			
		||||
 | 
			
		||||
  ## Filters
 | 
			
		||||
  chill.aside_activity.export.date_filter:
 | 
			
		||||
    class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
 | 
			
		||||
@@ -19,9 +31,34 @@ services:
 | 
			
		||||
    tags:
 | 
			
		||||
      - { name: chill.export_filter, alias: 'aside_activity_type_filter' }
 | 
			
		||||
 | 
			
		||||
  chill.aside_activity.export.user_job_filter:
 | 
			
		||||
      class: Chill\AsideActivityBundle\Export\Filter\ByUserJobFilter
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export_filter, alias: 'aside_activity_user_job_filter' }
 | 
			
		||||
 | 
			
		||||
  chill.aside_activity.export.user_scope_filter:
 | 
			
		||||
      class: Chill\AsideActivityBundle\Export\Filter\ByUserScopeFilter
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export_filter, alias: 'aside_activity_user_scope_filter' }
 | 
			
		||||
 | 
			
		||||
  chill.aside_activity.export.user_filter:
 | 
			
		||||
      class: Chill\AsideActivityBundle\Export\Filter\ByUserFilter
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export_filter, alias: 'aside_activity_user_filter' }
 | 
			
		||||
 | 
			
		||||
  ## Aggregators
 | 
			
		||||
 | 
			
		||||
  chill.aside_activity.export.type_aggregator:
 | 
			
		||||
    class: Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator
 | 
			
		||||
    tags:
 | 
			
		||||
      - { name: chill.export_aggregator, alias: activity_type_aggregator }
 | 
			
		||||
      - { name: chill.export_aggregator, alias: activity_type_aggregator }
 | 
			
		||||
 | 
			
		||||
  chill.aside_activity.export.user_job_aggregator:
 | 
			
		||||
      class: Chill\AsideActivityBundle\Export\Aggregator\ByUserJobAggregator
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export_aggregator, alias: aside_activity_user_job_aggregator }
 | 
			
		||||
 | 
			
		||||
  chill.aside_activity.export.user_scope_aggregator:
 | 
			
		||||
      class: Chill\AsideActivityBundle\Export\Aggregator\ByUserScopeAggregator
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export_aggregator, alias: aside_activity_user_scope_aggregator }
 | 
			
		||||
 
 | 
			
		||||
@@ -29,16 +29,16 @@ location: Lieu
 | 
			
		||||
 | 
			
		||||
# Crud
 | 
			
		||||
crud:
 | 
			
		||||
  aside_activity:
 | 
			
		||||
    title_view: Détail de l'activité annexe
 | 
			
		||||
    title_new: Nouvelle activité annexe
 | 
			
		||||
    title_edit: Édition d'une activité annexe
 | 
			
		||||
    title_delete: Supprimer une activité annexe
 | 
			
		||||
    button_delete: Supprimer
 | 
			
		||||
    confirm_message_delete: Êtes-vous sûr de vouloir supprimer cette activité annexe?
 | 
			
		||||
  aside_activity_category:
 | 
			
		||||
    title_new: Nouvelle catégorie d'activité annexe
 | 
			
		||||
    title_edit: Édition d'une catégorie de type d'activité
 | 
			
		||||
    aside_activity:
 | 
			
		||||
        title_view: Détail de l'activité annexe
 | 
			
		||||
        title_new: Nouvelle activité annexe
 | 
			
		||||
        title_edit: Édition d'une activité annexe
 | 
			
		||||
        title_delete: Supprimer une activité annexe
 | 
			
		||||
        button_delete: Supprimer
 | 
			
		||||
        confirm_message_delete: Êtes-vous sûr de vouloir supprimer cette activité annexe?
 | 
			
		||||
    aside_activity_category:
 | 
			
		||||
        title_new: Nouvelle catégorie d'activité annexe
 | 
			
		||||
        title_edit: Édition d'une catégorie de type d'activité
 | 
			
		||||
 | 
			
		||||
#forms
 | 
			
		||||
Create a new aside activity type: Nouvelle categorie d'activité annexe
 | 
			
		||||
@@ -169,18 +169,43 @@ Aside activity configuration: Configuration des activités annexes
 | 
			
		||||
 | 
			
		||||
# exports
 | 
			
		||||
export:
 | 
			
		||||
  Exports of aside activities: Exports des activités annexes
 | 
			
		||||
  Count aside activities: Nombre d'activités annexes
 | 
			
		||||
  Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
 | 
			
		||||
  filter:
 | 
			
		||||
    Filter by aside activity date: Filtrer les activités annexes par date
 | 
			
		||||
    Filter by aside activity type: Filtrer les activités annexes par type d'activité
 | 
			
		||||
    'Filtered by aside activity type: only %type%': "Filtré par type d'activité annexe: uniquement %type%"
 | 
			
		||||
    This date should be after the date given in "Implied in an aside activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités annexes après cette date"
 | 
			
		||||
    Aside activities after this date: Actvitités annexes après cette date
 | 
			
		||||
    Aside activities before this date: Actvitités annexes avant cette date
 | 
			
		||||
  aggregator:
 | 
			
		||||
    Group by aside activity type: Grouper les activités annexes par type d'activité
 | 
			
		||||
    Aside activity type: Type d'activité annexe
 | 
			
		||||
    aside_activity:
 | 
			
		||||
        List of aside activities: Liste des activités annexes
 | 
			
		||||
        createdAt: Création
 | 
			
		||||
        updatedAt: Dernière mise à jour
 | 
			
		||||
        agent_id: Utilisateur
 | 
			
		||||
        creator_id: Créateur
 | 
			
		||||
        main_scope: Service principal de l'utilisateur
 | 
			
		||||
        main_center: Centre principal de l'utilisteur
 | 
			
		||||
        aside_activity_type: Catégorie d'activité annexe
 | 
			
		||||
        date: Date
 | 
			
		||||
        duration: Durée
 | 
			
		||||
        note: Note
 | 
			
		||||
 | 
			
		||||
    Exports of aside activities: Exports des activités annexes
 | 
			
		||||
    Count aside activities: Nombre d'activités annexes
 | 
			
		||||
    Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
 | 
			
		||||
    Average aside activities duration: Durée moyenne des activités annexes
 | 
			
		||||
    Sum aside activities duration: Durée des activités annexes
 | 
			
		||||
    filter:
 | 
			
		||||
        Filter by aside activity date: Filtrer les activités annexes par date
 | 
			
		||||
        Filter by aside activity type: Filtrer les activités annexes par type d'activité
 | 
			
		||||
        'Filtered by aside activity type: only %type%': "Filtré par type d'activité annexe: uniquement %type%"
 | 
			
		||||
        Filtered by aside activities between %dateFrom% and %dateTo%: Filtré par date d'activité annexe, entre %dateFrom% et %dateTo%
 | 
			
		||||
        This date should be after the date given in "Implied in an aside activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités annexes après cette date"
 | 
			
		||||
        Aside activities after this date: Actvitités annexes après cette date
 | 
			
		||||
        Aside activities before this date: Actvitités annexes avant cette date
 | 
			
		||||
        'Filtered aside activity by user: only %users%': "Filtré par utilisateur: uniquement %users%"
 | 
			
		||||
        Filter aside activity by user: Filtrer par utilisateur
 | 
			
		||||
        'Filtered aside activities by user jobs: only %jobs%': "Filtré par métier des utilisateurs: uniquement %jobs%"
 | 
			
		||||
        Filter by user jobs: Filtrer les activités annexes par métier des utilisateurs
 | 
			
		||||
        'Filtered aside activities by user scope: only %scopes%': "Filtré par service des utilisateur: uniquement %scopes%"
 | 
			
		||||
        Filter by user scope: Filtrer les activités annexes par service d'utilisateur
 | 
			
		||||
    aggregator:
 | 
			
		||||
        Group by aside activity type: Grouper les activités annexes par type d'activité
 | 
			
		||||
        Aside activity type: Type d'activité annexe
 | 
			
		||||
        Aggregate by user job: Grouper les activités annexes par métier des utilisateurs
 | 
			
		||||
        Aggregate by user scope: Grouper les activités annexes par service des utilisateurs
 | 
			
		||||
 | 
			
		||||
# ROLES
 | 
			
		||||
CHILL_ASIDE_ACTIVITY_STATS: Statistiques pour les activités annexes
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,102 +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\BudgetBundle\Config;
 | 
			
		||||
 | 
			
		||||
class ConfigRepository
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    protected $charges;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    protected $resources;
 | 
			
		||||
 | 
			
		||||
    public function __construct($resources, $charges)
 | 
			
		||||
    {
 | 
			
		||||
        $this->resources = $resources;
 | 
			
		||||
        $this->charges = $charges;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getChargesKeys(bool $onlyActive = false): array
 | 
			
		||||
    {
 | 
			
		||||
        return array_map(static function ($element) {
 | 
			
		||||
            return $element['key'];
 | 
			
		||||
        }, $this->getCharges($onlyActive));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return array where keys are the resource'key and label the ressource label
 | 
			
		||||
     */
 | 
			
		||||
    public function getChargesLabels(bool $onlyActive = false)
 | 
			
		||||
    {
 | 
			
		||||
        $charges = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($this->getCharges($onlyActive) as $definition) {
 | 
			
		||||
            $charges[$definition['key']] = $this->normalizeLabel($definition['labels']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $charges;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getResourcesKeys(bool $onlyActive = false): array
 | 
			
		||||
    {
 | 
			
		||||
        return array_map(static function ($element) {
 | 
			
		||||
            return $element['key'];
 | 
			
		||||
        }, $this->getResources($onlyActive));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return array where keys are the resource'key and label the ressource label
 | 
			
		||||
     */
 | 
			
		||||
    public function getResourcesLabels(bool $onlyActive = false)
 | 
			
		||||
    {
 | 
			
		||||
        $resources = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($this->getResources($onlyActive) as $definition) {
 | 
			
		||||
            $resources[$definition['key']] = $this->normalizeLabel($definition['labels']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $resources;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getCharges(bool $onlyActive = false): array
 | 
			
		||||
    {
 | 
			
		||||
        return $onlyActive ?
 | 
			
		||||
            array_filter($this->charges, static function ($el) {
 | 
			
		||||
                return $el['active'];
 | 
			
		||||
            })
 | 
			
		||||
            : $this->charges;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getResources(bool $onlyActive = false): array
 | 
			
		||||
    {
 | 
			
		||||
        return $onlyActive ?
 | 
			
		||||
            array_filter($this->resources, static function ($el) {
 | 
			
		||||
                return $el['active'];
 | 
			
		||||
            })
 | 
			
		||||
            : $this->resources;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function normalizeLabel($labels)
 | 
			
		||||
    {
 | 
			
		||||
        $normalizedLabels = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($labels as $labelDefinition) {
 | 
			
		||||
            $normalizedLabels[$labelDefinition['lang']] = $labelDefinition['label'];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $normalizedLabels;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -35,7 +35,6 @@ class ChillBudgetExtension extends Extension implements PrependExtensionInterfac
 | 
			
		||||
        $config = $this->processConfiguration($configuration, $configs);
 | 
			
		||||
 | 
			
		||||
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../config'));
 | 
			
		||||
        $loader->load('services/config.yaml');
 | 
			
		||||
        $loader->load('services/form.yaml');
 | 
			
		||||
        $loader->load('services/repository.yaml');
 | 
			
		||||
        $loader->load('services/security.yaml');
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ abstract class AbstractElement
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(name="type", type="string", length=255)
 | 
			
		||||
     */
 | 
			
		||||
    private string $type;
 | 
			
		||||
    private string $type = '';
 | 
			
		||||
 | 
			
		||||
    /*Getters and Setters */
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,17 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\BudgetBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Type of charge.
 | 
			
		||||
 *
 | 
			
		||||
 * @ORM\Table(name="chill_budget.charge_type")
 | 
			
		||||
 * @ORM\Table(name="chill_budget.charge_type",
 | 
			
		||||
 *     uniqueConstraints={@ORM\UniqueConstraint(name="charge_kind_unique_type_idx", fields={"kind"})}
 | 
			
		||||
 * )
 | 
			
		||||
 * @ORM\Entity
 | 
			
		||||
 * @UniqueEntity(fields={"kind"})
 | 
			
		||||
 */
 | 
			
		||||
class ChargeKind
 | 
			
		||||
{
 | 
			
		||||
@@ -35,6 +40,8 @@ class ChargeKind
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="string", length=255, options={"default": ""}, nullable=false)
 | 
			
		||||
     * @Assert\Regex(pattern="/^[a-z0-9\-_]{1,}$/", message="budget.admin.form.kind.only_alphanumeric")
 | 
			
		||||
     * @Assert\Length(min=3)
 | 
			
		||||
     */
 | 
			
		||||
    private string $kind = '';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,17 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\BudgetBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Type of resource.
 | 
			
		||||
 *
 | 
			
		||||
 * @ORM\Table(name="chill_budget.resource_type")
 | 
			
		||||
 * @ORM\Table(name="chill_budget.resource_type", uniqueConstraints={
 | 
			
		||||
 *     @ORM\UniqueConstraint(name="resource_kind_unique_type_idx", fields={"kind"})
 | 
			
		||||
 * })
 | 
			
		||||
 * @ORM\Entity
 | 
			
		||||
 * @UniqueEntity(fields={"kind"})
 | 
			
		||||
 */
 | 
			
		||||
class ResourceKind
 | 
			
		||||
{
 | 
			
		||||
@@ -35,6 +40,8 @@ class ResourceKind
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="string", length=255, nullable=false, options={"default": ""})
 | 
			
		||||
     * @Assert\Regex(pattern="/^[a-z0-9\-_]{1,}$/", message="budget.admin.form.kind.only_alphanumeric")
 | 
			
		||||
     * @Assert\Length(min=3)
 | 
			
		||||
     */
 | 
			
		||||
    private string $kind = '';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ use Chill\MainBundle\Form\Type\TranslatableStringFormType;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +26,11 @@ class ChargeKindType extends AbstractType
 | 
			
		||||
    {
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('name', TranslatableStringFormType::class, [
 | 
			
		||||
                'label' => 'Nom',
 | 
			
		||||
                'label' => 'Title',
 | 
			
		||||
            ])
 | 
			
		||||
            ->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',
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('ordering', NumberType::class)
 | 
			
		||||
            ->add('isActive', CheckboxType::class, [
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ use Chill\MainBundle\Form\Type\TranslatableStringFormType;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +26,11 @@ class ResourceKindType extends AbstractType
 | 
			
		||||
    {
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('name', TranslatableStringFormType::class, [
 | 
			
		||||
                'label' => 'Nom',
 | 
			
		||||
                'label' => 'Title',
 | 
			
		||||
            ])
 | 
			
		||||
            ->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',
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('ordering', NumberType::class)
 | 
			
		||||
            ->add('isActive', CheckboxType::class, [
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\BudgetBundle\Form;
 | 
			
		||||
 | 
			
		||||
use Chill\BudgetBundle\Config\ConfigRepository;
 | 
			
		||||
use Chill\BudgetBundle\Entity\Charge;
 | 
			
		||||
use Chill\BudgetBundle\Entity\ChargeKind;
 | 
			
		||||
use Chill\BudgetBundle\Repository\ChargeKindRepository;
 | 
			
		||||
@@ -25,13 +24,9 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
use function array_flip;
 | 
			
		||||
use function asort;
 | 
			
		||||
 | 
			
		||||
class ChargeType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    protected ConfigRepository $configRepository;
 | 
			
		||||
 | 
			
		||||
    protected TranslatableStringHelperInterface $translatableStringHelper;
 | 
			
		||||
 | 
			
		||||
    private ChargeKindRepository $repository;
 | 
			
		||||
@@ -39,12 +34,10 @@ class ChargeType extends AbstractType
 | 
			
		||||
    private TranslatorInterface $translator;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        ConfigRepository $configRepository,
 | 
			
		||||
        TranslatableStringHelperInterface $translatableStringHelper,
 | 
			
		||||
        ChargeKindRepository $repository,
 | 
			
		||||
        TranslatorInterface $translator
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->configRepository = $configRepository;
 | 
			
		||||
        $this->translatableStringHelper = $translatableStringHelper;
 | 
			
		||||
        $this->repository = $repository;
 | 
			
		||||
        $this->translator = $translator;
 | 
			
		||||
@@ -116,19 +109,4 @@ class ChargeType extends AbstractType
 | 
			
		||||
    {
 | 
			
		||||
        return 'chill_budgetbundle_charge';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getTypes()
 | 
			
		||||
    {
 | 
			
		||||
        $charges = $this->configRepository
 | 
			
		||||
            ->getChargesLabels(true);
 | 
			
		||||
 | 
			
		||||
        // rewrite labels to filter in language
 | 
			
		||||
        foreach ($charges as $key => $labels) {
 | 
			
		||||
            $charges[$key] = $this->translatableStringHelper->localize($labels);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        asort($charges);
 | 
			
		||||
 | 
			
		||||
        return array_flip($charges);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\BudgetBundle\Form;
 | 
			
		||||
 | 
			
		||||
use Chill\BudgetBundle\Config\ConfigRepository;
 | 
			
		||||
use Chill\BudgetBundle\Entity\Resource;
 | 
			
		||||
use Chill\BudgetBundle\Entity\ResourceKind;
 | 
			
		||||
use Chill\BudgetBundle\Repository\ResourceKindRepository;
 | 
			
		||||
@@ -24,12 +23,9 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
use function array_flip;
 | 
			
		||||
 | 
			
		||||
class ResourceType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    protected ConfigRepository $configRepository;
 | 
			
		||||
 | 
			
		||||
    protected TranslatableStringHelperInterface $translatableStringHelper;
 | 
			
		||||
 | 
			
		||||
    private ResourceKindRepository $repository;
 | 
			
		||||
@@ -37,12 +33,10 @@ class ResourceType extends AbstractType
 | 
			
		||||
    private TranslatorInterface $translator;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        ConfigRepository $configRepository,
 | 
			
		||||
        TranslatableStringHelperInterface $translatableStringHelper,
 | 
			
		||||
        ResourceKindRepository $repository,
 | 
			
		||||
        TranslatorInterface $translator
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->configRepository = $configRepository;
 | 
			
		||||
        $this->translatableStringHelper = $translatableStringHelper;
 | 
			
		||||
        $this->repository = $repository;
 | 
			
		||||
        $this->translator = $translator;
 | 
			
		||||
@@ -98,19 +92,4 @@ class ResourceType extends AbstractType
 | 
			
		||||
    {
 | 
			
		||||
        return 'chill_budgetbundle_resource';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getTypes()
 | 
			
		||||
    {
 | 
			
		||||
        $resources = $this->configRepository
 | 
			
		||||
            ->getResourcesLabels(true);
 | 
			
		||||
 | 
			
		||||
        // rewrite labels to filter in language
 | 
			
		||||
        foreach ($resources as $key => $labels) {
 | 
			
		||||
            $resources[$key] = $this->translatableStringHelper->localize($labels);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        asort($resources);
 | 
			
		||||
 | 
			
		||||
        return array_flip($resources);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,8 @@ namespace Chill\BudgetBundle\Repository;
 | 
			
		||||
use Chill\BudgetBundle\Entity\ChargeKind;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
class ChargeKindRepository implements ObjectRepository
 | 
			
		||||
final class ChargeKindRepository implements ChargeKindRepositoryInterface
 | 
			
		||||
{
 | 
			
		||||
    private EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
@@ -77,6 +76,11 @@ class ChargeKindRepository implements ObjectRepository
 | 
			
		||||
        return $this->repository->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneByKind(string $kind): ?ChargeKind
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findOneBy(['kind' => $kind]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return ChargeKind::class;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
<?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\BudgetBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\BudgetBundle\Entity\ChargeKind;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
interface ChargeKindRepositoryInterface extends ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
    public function find($id): ?ChargeKind;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return ChargeType[]
 | 
			
		||||
     */
 | 
			
		||||
    public function findAll(): array;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return ChargeType[]
 | 
			
		||||
     */
 | 
			
		||||
    public function findAllActive(): array;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return ChargeType[]
 | 
			
		||||
     */
 | 
			
		||||
    public function findAllByType(string $type): array;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param mixed|null $limit
 | 
			
		||||
     * @param mixed|null $offset
 | 
			
		||||
     *
 | 
			
		||||
     * @return ChargeType[]
 | 
			
		||||
     */
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria): ?ChargeKind;
 | 
			
		||||
 | 
			
		||||
    public function findOneByKind(string $kind): ?ChargeKind;
 | 
			
		||||
 | 
			
		||||
    public function getClassName(): string;
 | 
			
		||||
}
 | 
			
		||||
@@ -14,9 +14,8 @@ namespace Chill\BudgetBundle\Repository;
 | 
			
		||||
use Chill\BudgetBundle\Entity\ResourceKind;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
class ResourceKindRepository implements ObjectRepository
 | 
			
		||||
final class ResourceKindRepository implements ResourceKindRepositoryInterface
 | 
			
		||||
{
 | 
			
		||||
    private EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
@@ -77,6 +76,11 @@ class ResourceKindRepository implements ObjectRepository
 | 
			
		||||
        return $this->repository->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneByKind(string $kind): ?ResourceKind
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findOneBy(['kind' => $kind]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return ResourceKind::class;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
<?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\BudgetBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\BudgetBundle\Entity\ResourceKind;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
interface ResourceKindRepositoryInterface extends ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
    public function find($id): ?ResourceKind;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return ResourceType[]
 | 
			
		||||
     */
 | 
			
		||||
    public function findAll(): array;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return ResourceType[]
 | 
			
		||||
     */
 | 
			
		||||
    public function findAllActive(): array;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return ResourceType[]
 | 
			
		||||
     */
 | 
			
		||||
    public function findAllByType(string $type): array;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param mixed|null $limit
 | 
			
		||||
     * @param mixed|null $offset
 | 
			
		||||
     *
 | 
			
		||||
     * @return ResourceType[]
 | 
			
		||||
     */
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria): ?ResourceKind;
 | 
			
		||||
 | 
			
		||||
    public function findOneByKind(string $kind): ?ResourceKind;
 | 
			
		||||
 | 
			
		||||
    public function getClassName(): string;
 | 
			
		||||
}
 | 
			
		||||
@@ -19,7 +19,10 @@
 | 
			
		||||
			{% for entity in entities %}
 | 
			
		||||
				<tr>
 | 
			
		||||
                    <td>{{ entity.ordering }}</td>
 | 
			
		||||
					<td>{{ entity|chill_entity_render_box }}</td>
 | 
			
		||||
					<td>
 | 
			
		||||
                        {{ entity|chill_entity_render_box }}<br/>
 | 
			
		||||
                        <strong>{{ 'budget.admin.form.Charge_kind_key'|trans }} :</strong> <code>{{ entity.kind }}</code>
 | 
			
		||||
                    </td>
 | 
			
		||||
					<td style="text-align:center;">
 | 
			
		||||
						{%- if entity.isActive -%}
 | 
			
		||||
							<i class="fa fa-check-square-o"></i>
 | 
			
		||||
@@ -39,6 +42,8 @@
 | 
			
		||||
		</tbody>
 | 
			
		||||
	</table>
 | 
			
		||||
 | 
			
		||||
    {{ chill_pagination(paginator) }}
 | 
			
		||||
 | 
			
		||||
	<ul class="record_actions sticky-form-buttons">
 | 
			
		||||
		<li>
 | 
			
		||||
			<a href="{{ path('chill_crud_charge_kind_new') }}" class="btn btn-create">
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,10 @@
 | 
			
		||||
			{% for entity in entities %}
 | 
			
		||||
				<tr>
 | 
			
		||||
                    <td>{{ entity.ordering }}</td>
 | 
			
		||||
					<td>{{ entity|chill_entity_render_box }}</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        {{ entity|chill_entity_render_box }}<br/>
 | 
			
		||||
                        <strong>{{ 'budget.admin.form.Resource_kind_key'|trans }} :</strong> <code>{{ entity.kind }}</code>
 | 
			
		||||
                    </td>
 | 
			
		||||
					<td style="text-align:center;">
 | 
			
		||||
						{%- if entity.isActive -%}
 | 
			
		||||
							<i class="fa fa-check-square-o"></i>
 | 
			
		||||
@@ -39,6 +42,8 @@
 | 
			
		||||
		</tbody>
 | 
			
		||||
	</table>
 | 
			
		||||
 | 
			
		||||
    {{ chill_pagination(paginator) }}
 | 
			
		||||
 | 
			
		||||
	<ul class="record_actions sticky-form-buttons">
 | 
			
		||||
		<li>
 | 
			
		||||
			<a href="{{ path('chill_crud_resource_kind_new') }}" class="btn btn-create">
 | 
			
		||||
 
 | 
			
		||||
@@ -11,45 +11,50 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\BudgetBundle\Service\Summary;
 | 
			
		||||
 | 
			
		||||
use Chill\BudgetBundle\Config\ConfigRepository;
 | 
			
		||||
use Chill\BudgetBundle\Entity\ChargeKind;
 | 
			
		||||
use Chill\BudgetBundle\Entity\ResourceKind;
 | 
			
		||||
use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface;
 | 
			
		||||
use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\Household\Household;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\Query\ResultSetMapping;
 | 
			
		||||
use LogicException;
 | 
			
		||||
use RuntimeException;
 | 
			
		||||
use function count;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helps to find a summary of the budget: the sum of resources and charges.
 | 
			
		||||
 */
 | 
			
		||||
class SummaryBudget implements SummaryBudgetInterface
 | 
			
		||||
final class SummaryBudget implements SummaryBudgetInterface
 | 
			
		||||
{
 | 
			
		||||
    private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
 | 
			
		||||
    private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, charge_id AS kind_id FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY charge_id';
 | 
			
		||||
 | 
			
		||||
    private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
 | 
			
		||||
    private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, charge_id AS kind_id FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY charge_id';
 | 
			
		||||
 | 
			
		||||
    private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
 | 
			
		||||
    private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, resource_id AS kind_id FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY resource_id';
 | 
			
		||||
 | 
			
		||||
    private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
 | 
			
		||||
    private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, resource_id AS kind_id FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY resource_id';
 | 
			
		||||
 | 
			
		||||
    private array $chargeLabels;
 | 
			
		||||
 | 
			
		||||
    private ConfigRepository $configRepository;
 | 
			
		||||
    private ChargeKindRepositoryInterface $chargeKindRepository;
 | 
			
		||||
 | 
			
		||||
    private EntityManagerInterface $em;
 | 
			
		||||
 | 
			
		||||
    private array $resourcesLabels;
 | 
			
		||||
    private ResourceKindRepositoryInterface $resourceKindRepository;
 | 
			
		||||
 | 
			
		||||
    private TranslatableStringHelperInterface $translatableStringHelper;
 | 
			
		||||
 | 
			
		||||
    public function __construct(EntityManagerInterface $em, ConfigRepository $configRepository, TranslatableStringHelperInterface $translatableStringHelper)
 | 
			
		||||
    {
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        EntityManagerInterface $em,
 | 
			
		||||
        TranslatableStringHelperInterface $translatableStringHelper,
 | 
			
		||||
        ResourceKindRepositoryInterface $resourceKindRepository,
 | 
			
		||||
        ChargeKindRepositoryInterface $chargeKindRepository
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->em = $em;
 | 
			
		||||
        $this->configRepository = $configRepository;
 | 
			
		||||
        $this->chargeLabels = $configRepository->getChargesLabels();
 | 
			
		||||
        $this->resourcesLabels = $configRepository->getResourcesLabels();
 | 
			
		||||
        $this->translatableStringHelper = $translatableStringHelper;
 | 
			
		||||
        $this->resourceKindRepository = $resourceKindRepository;
 | 
			
		||||
        $this->chargeKindRepository = $chargeKindRepository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getSummaryForHousehold(?Household $household): array
 | 
			
		||||
@@ -112,7 +117,7 @@ class SummaryBudget implements SummaryBudgetInterface
 | 
			
		||||
        $rsm = new ResultSetMapping();
 | 
			
		||||
        $rsm
 | 
			
		||||
            ->addScalarResult('sum', 'sum')
 | 
			
		||||
            ->addScalarResult('type', 'type')
 | 
			
		||||
            ->addScalarResult('kind_id', 'kind_id')
 | 
			
		||||
            ->addScalarResult('comment', 'comment');
 | 
			
		||||
 | 
			
		||||
        return $rsm;
 | 
			
		||||
@@ -120,51 +125,62 @@ class SummaryBudget implements SummaryBudgetInterface
 | 
			
		||||
 | 
			
		||||
    private function getEmptyChargeArray(): array
 | 
			
		||||
    {
 | 
			
		||||
        $keys = $this->configRepository->getChargesKeys();
 | 
			
		||||
        $labels = $this->chargeLabels;
 | 
			
		||||
        $keys = array_map(static fn (ChargeKind $kind) => $kind->getKind(), $this->chargeKindRepository->findAll());
 | 
			
		||||
 | 
			
		||||
        return array_combine($keys, array_map(function ($i) use ($labels) {
 | 
			
		||||
            return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => ''];
 | 
			
		||||
        return array_combine($keys, array_map(function ($kind) {
 | 
			
		||||
            return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->chargeKindRepository->findOneByKind($kind)->getName()), 'comment' => ''];
 | 
			
		||||
        }, $keys));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getEmptyResourceArray(): array
 | 
			
		||||
    {
 | 
			
		||||
        $keys = $this->configRepository->getResourcesKeys();
 | 
			
		||||
        $labels = $this->resourcesLabels;
 | 
			
		||||
        $keys = array_map(static fn (ResourceKind $kind) => $kind->getKind(), $this->resourceKindRepository->findAll());
 | 
			
		||||
 | 
			
		||||
        return array_combine($keys, array_map(function ($i) use ($labels) {
 | 
			
		||||
            return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => ''];
 | 
			
		||||
        return array_combine($keys, array_map(function ($kind) {
 | 
			
		||||
            return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->resourceKindRepository->findOneByKind($kind)->getName()), 'comment' => ''];
 | 
			
		||||
        }, $keys));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function rowToArray(array $rows, string $kind): array
 | 
			
		||||
    {
 | 
			
		||||
        $result = [];
 | 
			
		||||
 | 
			
		||||
        switch ($kind) {
 | 
			
		||||
            case 'charge':
 | 
			
		||||
                $label = $this->chargeLabels;
 | 
			
		||||
                foreach ($rows as $row) {
 | 
			
		||||
                    $chargeKind = $this->chargeKindRepository->find($row['kind_id']);
 | 
			
		||||
 | 
			
		||||
                break;
 | 
			
		||||
                    if (null === $chargeKind) {
 | 
			
		||||
                        throw new RuntimeException('charge kind not found: ' . $row['kind_id']);
 | 
			
		||||
                    }
 | 
			
		||||
                    $result[$chargeKind->getKind()] = [
 | 
			
		||||
                        'sum' => (float) $row['sum'],
 | 
			
		||||
                        'label' => $this->translatableStringHelper->localize($chargeKind->getName()),
 | 
			
		||||
                        'comment' => (string) $row['comment'],
 | 
			
		||||
                    ];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return $result;
 | 
			
		||||
 | 
			
		||||
            case 'resource':
 | 
			
		||||
                $label = $this->resourcesLabels;
 | 
			
		||||
                foreach ($rows as $row) {
 | 
			
		||||
                    $resourceKind = $this->resourceKindRepository->find($row['kind_id']);
 | 
			
		||||
 | 
			
		||||
                break;
 | 
			
		||||
                    if (null === $resourceKind) {
 | 
			
		||||
                        throw new RuntimeException('charge kind not found: ' . $row['kind_id']);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    $result[$resourceKind->getKind()] = [
 | 
			
		||||
                        'sum' => (float) $row['sum'],
 | 
			
		||||
                        'label' => $this->translatableStringHelper->localize($resourceKind->getName()),
 | 
			
		||||
                        'comment' => (string) $row['comment'],
 | 
			
		||||
                    ];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return $result;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                throw new LogicException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $result = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($rows as $row) {
 | 
			
		||||
            $result[$row['type']] = [
 | 
			
		||||
                'sum' => (float) $row['sum'],
 | 
			
		||||
                'label' => $this->translatableStringHelper->localize($label[$row['type']]),
 | 
			
		||||
                'comment' => (string) $row['comment'],
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $result;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,65 +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\BudgetBundle\Templating;
 | 
			
		||||
 | 
			
		||||
use Chill\BudgetBundle\Config\ConfigRepository;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
 | 
			
		||||
use Twig\Extension\AbstractExtension;
 | 
			
		||||
use Twig\TwigFilter;
 | 
			
		||||
use UnexpectedValueException;
 | 
			
		||||
 | 
			
		||||
class Twig extends AbstractExtension
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var ConfigRepository
 | 
			
		||||
     */
 | 
			
		||||
    protected $configRepository;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var TranslatableStringHelper
 | 
			
		||||
     */
 | 
			
		||||
    protected $translatableStringHelper;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        ConfigRepository $configRepository,
 | 
			
		||||
        TranslatableStringHelper $translatableStringHelper
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->configRepository = $configRepository;
 | 
			
		||||
        $this->translatableStringHelper = $translatableStringHelper;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function displayLink($link, $family)
 | 
			
		||||
    {
 | 
			
		||||
        switch ($family) {
 | 
			
		||||
            case 'resource':
 | 
			
		||||
                return $this->translatableStringHelper->localize(
 | 
			
		||||
                    $this->configRepository->getResourcesLabels()[$link]
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            case 'charge':
 | 
			
		||||
                return $this->translatableStringHelper->localize(
 | 
			
		||||
                    $this->configRepository->getChargesLabels()[$link]
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                throw new UnexpectedValueException("This family of element: {$family} is not "
 | 
			
		||||
                    . "supported. Supported families are 'resource', 'charge'");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getFilters()
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            new TwigFilter('budget_element_type_display', [$this, 'displayLink'], ['is_safe' => ['html']]),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,159 @@
 | 
			
		||||
<?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\BudgetBundle\Tests\Service\Summary;
 | 
			
		||||
 | 
			
		||||
use Chill\BudgetBundle\Entity\ChargeKind;
 | 
			
		||||
use Chill\BudgetBundle\Entity\ResourceKind;
 | 
			
		||||
use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface;
 | 
			
		||||
use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface;
 | 
			
		||||
use Chill\BudgetBundle\Service\Summary\SummaryBudget;
 | 
			
		||||
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
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
final class SummaryBudgetTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    public function testGenerateSummaryForPerson(): void
 | 
			
		||||
    {
 | 
			
		||||
        $queryCharges = $this->prophesize(AbstractQuery::class);
 | 
			
		||||
        $queryCharges->getResult()->willReturn([
 | 
			
		||||
            [
 | 
			
		||||
                'sum' => 250.0,
 | 
			
		||||
                'comment' => '',
 | 
			
		||||
                'kind_id' => 1, // kind: rental
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
        $queryCharges->setParameters(Argument::type('array'))
 | 
			
		||||
            ->will(static function ($args, $query) {
 | 
			
		||||
                return $query;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        $queryResources = $this->prophesize(AbstractQuery::class);
 | 
			
		||||
        $queryResources->getResult()->willReturn([
 | 
			
		||||
            [
 | 
			
		||||
                'sum' => 1500.0,
 | 
			
		||||
                'comment' => '',
 | 
			
		||||
                'kind_id' => 2, // kind: 'salary',
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
        $queryResources->setParameters(Argument::type('array'))
 | 
			
		||||
            ->will(static function ($args, $query) {
 | 
			
		||||
                return $query;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        $em = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $em->createNativeQuery(Argument::type('string'), Argument::type(Query\ResultSetMapping::class))
 | 
			
		||||
            ->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]);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        $chargeRepository = $this->prophesize(ChargeKindRepositoryInterface::class);
 | 
			
		||||
        $chargeRepository->findAll()->willReturn([
 | 
			
		||||
            $rental = (new ChargeKind())->setKind('rental')->setName(['fr' => 'Rental']),
 | 
			
		||||
            $other = (new ChargeKind())->setKind('other')->setName(['fr' => 'Other']),
 | 
			
		||||
        ]);
 | 
			
		||||
        $chargeRepository->find(1)->willReturn($rental);
 | 
			
		||||
        $chargeRepository->findOneByKind('rental')->willReturn($rental);
 | 
			
		||||
        $chargeRepository->findOneByKind('other')->willReturn($other);
 | 
			
		||||
 | 
			
		||||
        $resourceRepository = $this->prophesize(ResourceKindRepositoryInterface::class);
 | 
			
		||||
        $resourceRepository->findAll()->willReturn([
 | 
			
		||||
            $salary = (new ResourceKind())->setKind('salary')->setName(['fr' => 'Salary']),
 | 
			
		||||
            $misc = (new ResourceKind())->setKind('misc')->setName(['fr' => 'Misc']),
 | 
			
		||||
        ]);
 | 
			
		||||
        $resourceRepository->find(2)->willReturn($salary);
 | 
			
		||||
        $resourceRepository->findOneByKind('salary')->willReturn($salary);
 | 
			
		||||
        $resourceRepository->findOneByKind('misc')->willReturn($misc);
 | 
			
		||||
 | 
			
		||||
        $translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class);
 | 
			
		||||
        $translatableStringHelper->localize(Argument::type('array'))->will(static function ($arg) {
 | 
			
		||||
            return $arg[0]['fr'];
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $person = new Person();
 | 
			
		||||
        $personReflection = new ReflectionClass($person);
 | 
			
		||||
        $personIdReflection = $personReflection->getProperty('id');
 | 
			
		||||
        $personIdReflection->setAccessible(true);
 | 
			
		||||
        $personIdReflection->setValue($person, 1);
 | 
			
		||||
 | 
			
		||||
        $household = new 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'));
 | 
			
		||||
        $household->addMember($householdMember);
 | 
			
		||||
 | 
			
		||||
        $summaryBudget = new SummaryBudget(
 | 
			
		||||
            $em->reveal(),
 | 
			
		||||
            $translatableStringHelper->reveal(),
 | 
			
		||||
            $resourceRepository->reveal(),
 | 
			
		||||
            $chargeRepository->reveal()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $summary = $summaryBudget->getSummaryForPerson($person);
 | 
			
		||||
        $summaryForHousehold = $summaryBudget->getSummaryForHousehold($household);
 | 
			
		||||
 | 
			
		||||
        // we check the structure for the summary. The structure is the same for household
 | 
			
		||||
        // and persons
 | 
			
		||||
 | 
			
		||||
        $expected = [
 | 
			
		||||
            'charges' => [
 | 
			
		||||
                'rental' => ['sum' => 250.0, 'comment' => '', 'label' => 'Rental'],
 | 
			
		||||
                'other' => ['sum' => 0.0, 'comment' => '', 'label' => 'Other'],
 | 
			
		||||
            ],
 | 
			
		||||
            'resources' => [
 | 
			
		||||
                'salary' => ['sum' => 1500.0, 'comment' => '', 'label' => 'Salary'],
 | 
			
		||||
                'misc' => ['sum' => 0.0, 'comment' => '', 'label' => 'Misc'],
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        foreach ([$summaryForHousehold, $summary] as $summary) {
 | 
			
		||||
            $this->assertIsArray($summary);
 | 
			
		||||
            $this->assertEqualsCanonicalizing(['charges', 'resources'], array_keys($summary));
 | 
			
		||||
            $this->assertEqualsCanonicalizing(['rental', 'other'], array_keys($summary['charges']));
 | 
			
		||||
            $this->assertEqualsCanonicalizing(['salary', 'misc'], array_keys($summary['resources']));
 | 
			
		||||
 | 
			
		||||
            foreach ($expected as $resCha => $contains) {
 | 
			
		||||
                foreach ($contains as $kind => $row) {
 | 
			
		||||
                    $this->assertEqualsCanonicalizing($row, $summary[$resCha][$kind]);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
services:
 | 
			
		||||
    Chill\BudgetBundle\Config\ConfigRepository:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $resources: '%chill_budget.resources%'
 | 
			
		||||
            $charges: '%chill_budget.charges%'
 | 
			
		||||
@@ -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\Budget;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Auto-generated Migration: Please modify to your needs!
 | 
			
		||||
 */
 | 
			
		||||
final class Version20230201131008 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        // this shouldn't be undone.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Fix the comment on tags column in resource and charge type';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_budget.resource_type.tags IS \'(DC2Type:json)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_budget.charge_type.tags IS \'(DC2Type:json)\'');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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\Budget;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
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';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql("UPDATE chill_budget.resource_type SET kind=md5(random()::text) WHERE kind = ''");
 | 
			
		||||
        $this->addSql("UPDATE chill_budget.charge_type SET kind=md5(random()::text) WHERE kind = ''");
 | 
			
		||||
        $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);');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -77,6 +77,20 @@ The balance: Différence entre ressources et charges
 | 
			
		||||
Valid since %startDate% until %endDate%: Valide depuis le %startDate% jusqu'au %endDate%
 | 
			
		||||
Valid since %startDate%: Valide depuis le %startDate%
 | 
			
		||||
 | 
			
		||||
budget:
 | 
			
		||||
    admin:
 | 
			
		||||
        form:
 | 
			
		||||
            Charge_kind_key: Clé d'identification
 | 
			
		||||
            Resource_kind_key: Clé d'identification
 | 
			
		||||
            This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document: Cette clé sert à identifier le type de charge ou de revenu lors de la génération de document. Seuls les caractères alpha-numériques sont autorisés. Modifier cette clé peut avoir un effet lors de la génération de nouveaux documents.
 | 
			
		||||
 | 
			
		||||
# ROLES
 | 
			
		||||
Budget elements: Budget
 | 
			
		||||
CHILL_BUDGET_ELEMENT_CREATE: Créer une ressource/charge
 | 
			
		||||
CHILL_BUDGET_ELEMENT_DELETE: Supprimer une ressource/charge
 | 
			
		||||
CHILL_BUDGET_ELEMENT_SEE: Voir les ressources/charges
 | 
			
		||||
CHILL_BUDGET_ELEMENT_UPDATE: Modifier une ressource/charge
 | 
			
		||||
 | 
			
		||||
## admin
 | 
			
		||||
 | 
			
		||||
crud:
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +1,8 @@
 | 
			
		||||
The amount cannot be empty: Le montant ne peut pas être vide ou égal à zéro
 | 
			
		||||
The budget element's end date must be after the start date: La date de fin doit être après la date de début
 | 
			
		||||
The budget element's end date must be after the start date: La date de fin doit être après la date de début
 | 
			
		||||
 | 
			
		||||
budget:
 | 
			
		||||
    admin:
 | 
			
		||||
        form:
 | 
			
		||||
            kind:
 | 
			
		||||
                only_alphanumeric
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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) }}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) }}
 | 
			
		||||
 
 | 
			
		||||
@@ -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' } %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -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');
 | 
			
		||||
 
 | 
			
		||||
@@ -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é
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,98 +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()) {
 | 
			
		||||
            // very ugly hack...
 | 
			
		||||
            dd($context->getData($template, $entity, $contextGenerationData));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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',
 | 
			
		||||
                ]
 | 
			
		||||
            return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [
 | 
			
		||||
                'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT),
 | 
			
		||||
            ]);
 | 
			
		||||
        } 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',
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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 }}
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
    <title>{{ 'Doc generator debug'|trans }}</title>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<pre>{{ datas }}</pre>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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'
 | 
			
		||||
 
 | 
			
		||||
@@ -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');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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(),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
 | 
			
		||||
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
 | 
			
		||||
import {createApp} from "vue";
 | 
			
		||||
import {StoredObject, StoredObjectStatusChange} from "../../types";
 | 
			
		||||
import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers";
 | 
			
		||||
 | 
			
		||||
const i18n = _createI18n({});
 | 
			
		||||
 | 
			
		||||
window.addEventListener('DOMContentLoaded', function (e) {
 | 
			
		||||
  document.querySelectorAll<HTMLDivElement>('div[data-download-buttons]').forEach((el) => {
 | 
			
		||||
     const app = createApp({
 | 
			
		||||
       components: {DocumentActionButtonsGroup},
 | 
			
		||||
       data() {
 | 
			
		||||
 | 
			
		||||
         const datasets = el.dataset as {
 | 
			
		||||
           filename: string,
 | 
			
		||||
           canEdit: string,
 | 
			
		||||
           storedObject: string,
 | 
			
		||||
           buttonSmall: string,
 | 
			
		||||
         };
 | 
			
		||||
 | 
			
		||||
         const
 | 
			
		||||
           storedObject = JSON.parse(datasets.storedObject) as StoredObject,
 | 
			
		||||
           filename = datasets.filename,
 | 
			
		||||
           canEdit = datasets.canEdit === '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" @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);
 | 
			
		||||
  })
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										35
									
								
								src/Bundle/ChillDocStoreBundle/Resources/public/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/Bundle/ChillDocStoreBundle/Resources/public/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import {DateTime} from "../../../ChillMainBundle/Resources/public/types";
 | 
			
		||||
 | 
			
		||||
export type StoredObjectStatus = "ready"|"failure"|"pending";
 | 
			
		||||
 | 
			
		||||
export interface StoredObject {
 | 
			
		||||
  id: number,
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * filename of the object in the object storage
 | 
			
		||||
   */
 | 
			
		||||
  filename: string,
 | 
			
		||||
  creationDate: DateTime,
 | 
			
		||||
  datas: object,
 | 
			
		||||
  iv: number[],
 | 
			
		||||
  keyInfos: object,
 | 
			
		||||
  title: string,
 | 
			
		||||
  type: string,
 | 
			
		||||
  uuid: string,
 | 
			
		||||
  status: StoredObjectStatus,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface StoredObjectStatusChange {
 | 
			
		||||
  id: number,
 | 
			
		||||
  filename: string,
 | 
			
		||||
  status: StoredObjectStatus,
 | 
			
		||||
  type: string,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Function executed by the WopiEditButton component.
 | 
			
		||||
 */
 | 
			
		||||
export type WopiEditButtonExecutableBeforeLeaveFunction = {
 | 
			
		||||
  (): Promise<void>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,123 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <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">
 | 
			
		||||
      <li v-if="props.canEdit && is_extension_editable(props.storedObject.type)">
 | 
			
		||||
        <wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf">
 | 
			
		||||
        <convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li v-if="props.canDownload">
 | 
			
		||||
        <download-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></download-button>
 | 
			
		||||
      </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, is_object_ready} from "./StoredObjectButton/helpers";
 | 
			
		||||
import {
 | 
			
		||||
  StoredObject,
 | 
			
		||||
  StoredObjectStatusChange,
 | 
			
		||||
  WopiEditButtonExecutableBeforeLeaveFunction
 | 
			
		||||
} from "../types";
 | 
			
		||||
 | 
			
		||||
interface DocumentActionButtonsGroupConfig {
 | 
			
		||||
  storedObject: StoredObject,
 | 
			
		||||
  small?: boolean,
 | 
			
		||||
  canEdit?: boolean,
 | 
			
		||||
  canDownload?: boolean,
 | 
			
		||||
  canConvertPdf?: boolean,
 | 
			
		||||
  returnPath?: string,
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Will be the filename displayed to the user when he·she download the document
 | 
			
		||||
   * (the document will be saved on his disk with this name)
 | 
			
		||||
   *
 | 
			
		||||
   * If not set, 'document' will be used.
 | 
			
		||||
   */
 | 
			
		||||
  filename?: string,
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * If set, will execute this function before leaving to the editor
 | 
			
		||||
   */
 | 
			
		||||
  executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (e: 'onStoredObjectStatusChange', newStatus: StoredObjectStatusChange): void
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
 | 
			
		||||
  small: false,
 | 
			
		||||
  canEdit: true,
 | 
			
		||||
  canDownload: true,
 | 
			
		||||
  canConvertPdf: true,
 | 
			
		||||
  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>
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,5 @@
 | 
			
		||||
# About buttons and components available
 | 
			
		||||
 | 
			
		||||
## DocumentActionButtonsGroup
 | 
			
		||||
 | 
			
		||||
This is an component to use to render a group of button with actions linked to a document.
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a :class="props.classes" @click="download_and_open($event)">
 | 
			
		||||
    <i class="fa fa-file-pdf-o"></i>
 | 
			
		||||
    Télécharger en pdf
 | 
			
		||||
  </a>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
 | 
			
		||||
import {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers";
 | 
			
		||||
import mime from "mime";
 | 
			
		||||
import {reactive} from "vue";
 | 
			
		||||
import {StoredObject} from "../../types";
 | 
			
		||||
 | 
			
		||||
interface ConvertButtonConfig {
 | 
			
		||||
  storedObject: StoredObject,
 | 
			
		||||
  classes: { [key: string]: boolean},
 | 
			
		||||
  filename?: string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface DownloadButtonState {
 | 
			
		||||
  content: null|string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<ConvertButtonConfig>();
 | 
			
		||||
const state: DownloadButtonState = reactive({content: null});
 | 
			
		||||
 | 
			
		||||
async function download_and_open(event: Event): Promise<void> {
 | 
			
		||||
  const button = event.target as HTMLAnchorElement;
 | 
			
		||||
 | 
			
		||||
  if (null === state.content) {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
 | 
			
		||||
    const raw = await download_doc(build_convert_link(props.storedObject.uuid));
 | 
			
		||||
    state.content = window.URL.createObjectURL(raw);
 | 
			
		||||
 | 
			
		||||
    button.href = window.URL.createObjectURL(raw);
 | 
			
		||||
    button.type = 'application/pdf';
 | 
			
		||||
 | 
			
		||||
    button.download = (props.filename + '.pdf') || 'document.pdf';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  button.click();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a :class="props.classes" @click="download_and_open($event)">
 | 
			
		||||
    <i class="fa fa-download"></i>
 | 
			
		||||
    Télécharger
 | 
			
		||||
  </a>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import {reactive} from "vue";
 | 
			
		||||
import {build_download_info_link, download_and_decrypt_doc} from "./helpers";
 | 
			
		||||
import mime from "mime";
 | 
			
		||||
import {StoredObject} from "../../types";
 | 
			
		||||
 | 
			
		||||
interface DownloadButtonConfig {
 | 
			
		||||
  storedObject: StoredObject,
 | 
			
		||||
  classes: {[k: string]: boolean},
 | 
			
		||||
  filename?: string,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DownloadButtonState {
 | 
			
		||||
  content: null|string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<DownloadButtonConfig>();
 | 
			
		||||
const state: DownloadButtonState = reactive({content: null});
 | 
			
		||||
 | 
			
		||||
async function download_and_open(event: Event): Promise<void> {
 | 
			
		||||
  const button = event.target as HTMLAnchorElement;
 | 
			
		||||
 | 
			
		||||
  if (null === state.content) {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
 | 
			
		||||
    const urlInfo = build_download_info_link(props.storedObject.filename);
 | 
			
		||||
 | 
			
		||||
    const raw = await download_and_decrypt_doc(urlInfo, props.storedObject.keyInfos, new Uint8Array(props.storedObject.iv));
 | 
			
		||||
    state.content = window.URL.createObjectURL(raw);
 | 
			
		||||
 | 
			
		||||
    button.href = window.URL.createObjectURL(raw);
 | 
			
		||||
    button.type = props.storedObject.type;
 | 
			
		||||
 | 
			
		||||
    button.download = props.filename || 'document';
 | 
			
		||||
 | 
			
		||||
    const ext = mime.getExtension(props.storedObject.type);
 | 
			
		||||
    if (null !== ext) {
 | 
			
		||||
      button.download = button.download + '.' + ext;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  button.click();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a :class="Object.assign(props.classes, {'btn': true})" @click="beforeLeave($event)" :href="build_wopi_editor_link(props.storedObject.uuid, props.returnPath)">
 | 
			
		||||
    <i class="fa fa-paragraph"></i>
 | 
			
		||||
    Editer en ligne
 | 
			
		||||
  </a>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import WopiEditButton from "./WopiEditButton.vue";
 | 
			
		||||
import {build_wopi_editor_link} from "./helpers";
 | 
			
		||||
import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
 | 
			
		||||
 | 
			
		||||
interface WopiEditButtonConfig {
 | 
			
		||||
  storedObject: StoredObject,
 | 
			
		||||
  returnPath?: string,
 | 
			
		||||
  classes: {[k: string] : boolean},
 | 
			
		||||
  executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<WopiEditButtonConfig>();
 | 
			
		||||
 | 
			
		||||
let executed = false;
 | 
			
		||||
 | 
			
		||||
async function beforeLeave(event: Event): Promise<true> {
 | 
			
		||||
  console.log(executed);
 | 
			
		||||
  if (props.executeBeforeLeave === undefined || executed === true) {
 | 
			
		||||
    return Promise.resolve(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  event.preventDefault();
 | 
			
		||||
 | 
			
		||||
  await props.executeBeforeLeave();
 | 
			
		||||
  executed = true;
 | 
			
		||||
 | 
			
		||||
  const link = event.target as HTMLAnchorElement;
 | 
			
		||||
  link.click();
 | 
			
		||||
 | 
			
		||||
  return Promise.resolve(true);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="sass">
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,193 @@
 | 
			
		||||
import {StoredObject, StoredObjectStatus, StoredObjectStatusChange} from "../../types";
 | 
			
		||||
 | 
			
		||||
const MIMES_EDIT = new Set([
 | 
			
		||||
  'application/vnd.ms-powerpoint',
 | 
			
		||||
  'application/vnd.ms-excel',
 | 
			
		||||
  'application/vnd.oasis.opendocument.text',
 | 
			
		||||
  'application/vnd.oasis.opendocument.text-flat-xml',
 | 
			
		||||
  'application/vnd.oasis.opendocument.spreadsheet',
 | 
			
		||||
  'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
 | 
			
		||||
  'application/vnd.oasis.opendocument.presentation',
 | 
			
		||||
  'application/vnd.oasis.opendocument.presentation-flat-xml',
 | 
			
		||||
  'application/vnd.oasis.opendocument.graphics',
 | 
			
		||||
  'application/vnd.oasis.opendocument.graphics-flat-xml',
 | 
			
		||||
  'application/vnd.oasis.opendocument.chart',
 | 
			
		||||
  'application/msword',
 | 
			
		||||
  'application/vnd.ms-excel',
 | 
			
		||||
  'application/vnd.ms-powerpoint',
 | 
			
		||||
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
 | 
			
		||||
  'application/vnd.ms-word.document.macroEnabled.12',
 | 
			
		||||
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
 | 
			
		||||
  'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
 | 
			
		||||
  'application/vnd.ms-excel.sheet.macroEnabled.12',
 | 
			
		||||
  'application/vnd.openxmlformats-officedocument.presentationml.presentation',
 | 
			
		||||
  'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
 | 
			
		||||
  'application/x-dif-document',
 | 
			
		||||
  'text/spreadsheet',
 | 
			
		||||
  'text/csv',
 | 
			
		||||
  'application/x-dbase',
 | 
			
		||||
  'text/rtf',
 | 
			
		||||
  'text/plain',
 | 
			
		||||
  'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const MIMES_VIEW = new Set([
 | 
			
		||||
  ...MIMES_EDIT,
 | 
			
		||||
  [
 | 
			
		||||
    'image/svg+xml',
 | 
			
		||||
    'application/vnd.sun.xml.writer',
 | 
			
		||||
    'application/vnd.sun.xml.calc',
 | 
			
		||||
    'application/vnd.sun.xml.impress',
 | 
			
		||||
    'application/vnd.sun.xml.draw',
 | 
			
		||||
    'application/vnd.sun.xml.writer.global',
 | 
			
		||||
    'application/vnd.sun.xml.writer.template',
 | 
			
		||||
    'application/vnd.sun.xml.calc.template',
 | 
			
		||||
    'application/vnd.sun.xml.impress.template',
 | 
			
		||||
    'application/vnd.sun.xml.draw.template',
 | 
			
		||||
    'application/vnd.oasis.opendocument.text-master',
 | 
			
		||||
    'application/vnd.oasis.opendocument.text-template',
 | 
			
		||||
    'application/vnd.oasis.opendocument.text-master-template',
 | 
			
		||||
    'application/vnd.oasis.opendocument.spreadsheet-template',
 | 
			
		||||
    'application/vnd.oasis.opendocument.presentation-template',
 | 
			
		||||
    'application/vnd.oasis.opendocument.graphics-template',
 | 
			
		||||
    'application/vnd.ms-word.template.macroEnabled.12',
 | 
			
		||||
    'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
 | 
			
		||||
    'application/vnd.ms-excel.template.macroEnabled.12',
 | 
			
		||||
    'application/vnd.openxmlformats-officedocument.presentationml.template',
 | 
			
		||||
    'application/vnd.ms-powerpoint.template.macroEnabled.12',
 | 
			
		||||
    'application/vnd.wordperfect',
 | 
			
		||||
    'application/x-aportisdoc',
 | 
			
		||||
    'application/x-hwp',
 | 
			
		||||
    'application/vnd.ms-works',
 | 
			
		||||
    'application/x-mswrite',
 | 
			
		||||
    'application/vnd.lotus-1-2-3',
 | 
			
		||||
    'image/cgm',
 | 
			
		||||
    'image/vnd.dxf',
 | 
			
		||||
    'image/x-emf',
 | 
			
		||||
    'image/x-wmf',
 | 
			
		||||
    'application/coreldraw',
 | 
			
		||||
    'application/vnd.visio2013',
 | 
			
		||||
    'application/vnd.visio',
 | 
			
		||||
    'application/vnd.ms-visio.drawing',
 | 
			
		||||
    'application/x-mspublisher',
 | 
			
		||||
    'application/x-sony-bbeb',
 | 
			
		||||
    'application/x-gnumeric',
 | 
			
		||||
    'application/macwriteii',
 | 
			
		||||
    'application/x-iwork-numbers-sffnumbers',
 | 
			
		||||
    'application/vnd.oasis.opendocument.text-web',
 | 
			
		||||
    'application/x-pagemaker',
 | 
			
		||||
    'application/x-fictionbook+xml',
 | 
			
		||||
    'application/clarisworks',
 | 
			
		||||
    'image/x-wpg',
 | 
			
		||||
    'application/x-iwork-pages-sffpages',
 | 
			
		||||
    'application/x-iwork-keynote-sffkey',
 | 
			
		||||
    'application/x-abiword',
 | 
			
		||||
    'image/x-freehand',
 | 
			
		||||
    'application/vnd.sun.xml.chart',
 | 
			
		||||
    'application/x-t602',
 | 
			
		||||
    'image/bmp',
 | 
			
		||||
    'image/png',
 | 
			
		||||
    'image/gif',
 | 
			
		||||
    'image/tiff',
 | 
			
		||||
    'image/jpg',
 | 
			
		||||
    'image/jpeg',
 | 
			
		||||
    'application/pdf',
 | 
			
		||||
  ]
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
function is_extension_editable(mimeType: string): boolean {
 | 
			
		||||
  return MIMES_EDIT.has(mimeType);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function is_extension_viewable(mimeType: string): boolean {
 | 
			
		||||
  return MIMES_VIEW.has(mimeType);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function build_convert_link(uuid: string) {
 | 
			
		||||
  return `/chill/wopi/convert/${uuid}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function build_download_info_link(object_name: string) {
 | 
			
		||||
  return `/asyncupload/temp_url/generate/GET?object_name=${object_name}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function build_wopi_editor_link(uuid: string, returnPath?: string) {
 | 
			
		||||
  if (returnPath === undefined) {
 | 
			
		||||
    returnPath = window.location.pathname + window.location.search + window.location.hash;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return `/chill/wopi/edit/${uuid}?returnPath=` + encodeURIComponent(returnPath);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function download_doc(url: string): Promise<Blob> {
 | 
			
		||||
  return window.fetch(url).then(r => {
 | 
			
		||||
    if (r.ok) {
 | 
			
		||||
      return r.blob()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw new Error('Could not download document');
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKey, iv: Uint8Array): Promise<Blob>
 | 
			
		||||
{
 | 
			
		||||
   const algo = 'AES-CBC';
 | 
			
		||||
   // get an url to download the object
 | 
			
		||||
   const downloadInfoResponse = await window.fetch(urlGenerator);
 | 
			
		||||
 | 
			
		||||
   if (!downloadInfoResponse.ok) {
 | 
			
		||||
     throw new Error("error while downloading url " + downloadInfoResponse.status + " " + downloadInfoResponse.statusText);
 | 
			
		||||
   }
 | 
			
		||||
 | 
			
		||||
   const downloadInfo = await downloadInfoResponse.json() as {url: string};
 | 
			
		||||
   const rawResponse = await window.fetch(downloadInfo.url);
 | 
			
		||||
 | 
			
		||||
   if (!rawResponse.ok) {
 | 
			
		||||
     throw new Error("error while downloading raw file " + rawResponse.status + " " + rawResponse.statusText);
 | 
			
		||||
   }
 | 
			
		||||
 | 
			
		||||
   if (iv.length === 0) {
 | 
			
		||||
     return rawResponse.blob();
 | 
			
		||||
   }
 | 
			
		||||
 | 
			
		||||
   const rawBuffer = await rawResponse.arrayBuffer();
 | 
			
		||||
 | 
			
		||||
   try {
 | 
			
		||||
     const key = await window.crypto.subtle
 | 
			
		||||
       .importKey('jwk', keyData, { name: algo }, false, ['decrypt']);
 | 
			
		||||
     const decrypted = await window.crypto.subtle
 | 
			
		||||
       .decrypt({ name: algo, iv: iv }, key, rawBuffer);
 | 
			
		||||
 | 
			
		||||
     return Promise.resolve(new Blob([decrypted]));
 | 
			
		||||
   } catch (e) {
 | 
			
		||||
     console.error('get error while keys and decrypt operations');
 | 
			
		||||
     console.error(e);
 | 
			
		||||
 | 
			
		||||
     throw e;
 | 
			
		||||
   }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
  build_wopi_editor_link,
 | 
			
		||||
  download_and_decrypt_doc,
 | 
			
		||||
  download_doc,
 | 
			
		||||
  is_extension_editable,
 | 
			
		||||
  is_extension_viewable,
 | 
			
		||||
  is_object_ready,
 | 
			
		||||
};
 | 
			
		||||
@@ -46,21 +46,8 @@
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            <li>
 | 
			
		||||
                {{ m.download_button(document.object, document.title) }}
 | 
			
		||||
                {{ document.object|chill_document_button_group(document.title, not freezed) }}
 | 
			
		||||
            </li>
 | 
			
		||||
            {% if chill_document_is_editable(document.object) %}
 | 
			
		||||
                {% if not freezed %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    {{ document.object|chill_document_edit_button({'title': document.title|e('html') }) }}
 | 
			
		||||
                </li>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a class="btn btn-wopilink disabled" href="#" title="{{ 'workflow.freezed document'|trans }}">
 | 
			
		||||
                            {{ 'Update document'|trans }}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE', document) and document.course != null %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': document.course.id, 'id': document.id}) }}" class="btn btn-show"></a>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,16 +8,16 @@
 | 
			
		||||
 | 
			
		||||
{% block js %}
 | 
			
		||||
	{{ parent() }}
 | 
			
		||||
	{{ encore_entry_script_tags('mod_async_upload') }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_docgen_picktemplate') }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_entity_workflow_pick') }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_document_action_buttons_group') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block css %}
 | 
			
		||||
	{{ parent() }}
 | 
			
		||||
	{{ encore_entry_link_tags('mod_async_upload') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_docgen_picktemplate') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_entity_workflow_pick') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_document_action_buttons_group') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user