Compare commits

..

1 Commits

Author SHA1 Message Date
527285bb13 Remove redundant line to create edit_form 2025-01-20 12:31:38 +01:00
174 changed files with 1275 additions and 4535 deletions

View File

@@ -1,62 +0,0 @@
## v3.7.0 - 2025-01-21
### Feature
* Use the Notifier component from Symfony to sens short messages (SMS). This allow to use more provider.
### Fixed
* ([#348](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/348)) [export] Fix aggregation of referrer's scope and job: fix the date range comparison
### Warning on configuration of Notifier component
If installed in an symfony app where the recipes are activated, this configuration should be added automatically:
```yaml
framework:
notifier:
chatter_transports:
texter_transports:
ovhcloud: '%env(OVHCLOUD_DSN)%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }
```
Actually, you should either:
- remove the configuration of ovhcloud added by the recipe
- or remove the previous configuration of chill, to avoid keeping legacy configuration
#### Remove the added configuration and keep the legacy configuration
To remove the configuration:
```diff
framework:
notifier:
chatter_transports:
texter_transports:
- ovhcloud: '%env(OVHCLOUD_DSN)%'
```
In that case, the previous configuration, which was stored under the `chill_main.short_messages.dsn` will be reconfigured into the Notifier component's configuration.
#### Properly configure SMS
You can also properly configure it, as [described in the OVH cloud provider repository](https://github.com/symfony/ovh-cloud-notifier/tree/5.4?tab=readme-ov-file#dsn-example) (where the scheme is `ovhcloud`):
**NOTE**: You have access to all notifier available with the [Notifier component](https://symfony.com/doc/current/notifier.html#notifier-sms-channel). You are not restricted to use OVH as a provider.
```diff
framework:
notifier:
chatter_transports:
texter_transports:
+ ovhcloud: '%env(OVHCLOUD_DSN)%' # this value should be located in a variable, and have `ovhcloud://` as a scheme
chill_main:
- short_messages:
- dsn: '%env(string:SHORT_MESSAGE_DSN)%'
```

View File

@@ -1,3 +0,0 @@
## v3.7.1 - 2025-01-21
### Fixed
* Fix legacy configuration processor for notifier component

View File

@@ -1,11 +0,0 @@
## v3.8.0 - 2025-02-03
### Feature
* Improve the UX of the news item admin form to prevent wrong usage
* ([#319](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/319)) Notification list: display the concerned person's badges in the list
* ([#320](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/320)) Show the first 3 persons directly in the accompanying period's banner
* ([#334](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/334)) Suggest current user when creating an activity
* ([#331](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/331)) Add attachments to workflows
### Fixed
* ([#350](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/350)) Add validation error to manual selection of person in PersonDuplicateController
* ([#354](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/354)) Fix document category creation
* ([#351](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/351)) Add definitive whitespace between span elements in vue PersonText component

View File

@@ -1,3 +0,0 @@
## v3.8.1 - 2025-02-05
### Fixed
* Fix household link in the parcours banner

View File

@@ -7,29 +7,15 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
kindFormat: '### {{.Kind}}'
# Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description.
changeFormat: >-
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ .Body }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }}
**Schema Change**: {{ .Custom.SchemaChange }}
{{- end -}}
{{ if and (.Custom.Long) (not (eq .Custom.Long "")) }}{{ .Custom.Long }}{{ end }}
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{.Body}} {{ if and (.Custom.Long) (not (eq .Custom.Long "")) }}
{{ .Custom.Long }}{{ end }}
custom:
- key: SchemaChange
label: Is a schema change required?
optional: false
type: enum
enumOptions:
- "No schema change"
- "Add columns or tables"
- "Drop or rename table or columns, or enforce new constraint that must be manually fixed"
- key: Issue
label: Issue number (on chill-bundles repository) (optional)
optional: true
type: int
minInt: 1
body:
# allow multiline messages
block: true

4
.env
View File

@@ -88,7 +88,3 @@ REDIS_HOST=redis
REDIS_PORT=6379
REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
###< chill-project/chill-bundles ###
###> symfony/ovh-cloud-notifier ###
# OVHCLOUD_DSN=ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME
###< symfony/ovh-cloud-notifier ###

1
.gitignore vendored
View File

@@ -5,7 +5,6 @@ composer.lock
docs/build/
.php_cs.cache
.cache/*
yarn.lock
docker/db/data
docker/rabbitmq/data

View File

@@ -25,7 +25,7 @@ $config = new PhpCsFixer\Config();
$config
->setFinder($finder)
->setRiskyAllowed(true)
->setCacheFile('var/php-cs-fixer.cache')
->setCacheFile('.cache/php-cs-fixer.cache')
->setUsingCache(true)
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
;

View File

@@ -6,89 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v3.8.1 - 2025-02-05
### Fixed
* Fix household link in the parcours banner
## v3.8.0 - 2025-02-03
### Feature
* Improve the UX of the news item admin form to prevent wrong usage
* ([#319](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/319)) Notification list: display the concerned person's badges in the list
* ([#320](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/320)) Show the first 3 persons directly in the accompanying period's banner
* ([#334](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/334)) Suggest current user when creating an activity
* ([#331](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/331)) Add attachments to workflows
### Fixed
* ([#350](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/350)) Add validation error to manual selection of person in PersonDuplicateController
* ([#354](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/354)) Fix document category creation
* ([#351](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/351)) Add definitive whitespace between span elements in vue PersonText component
## v3.7.1 - 2025-01-21
### Fixed
* Fix legacy configuration processor for notifier component
## v3.7.0 - 2025-01-21
### Feature
* Use the Notifier component from Symfony to sens short messages (SMS). This allow to use more provider.
### Fixed
* ([#348](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/348)) [export] Fix aggregation of referrer's scope and job: fix the date range comparison
### Warning on configuration of Notifier component
If installed in an symfony app where the recipes are activated, this configuration should be added automatically:
```yaml
framework:
notifier:
chatter_transports:
texter_transports:
ovhcloud: '%env(OVHCLOUD_DSN)%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }
```
Actually, you should either:
- remove the configuration of ovhcloud added by the recipe
- or remove the previous configuration of chill, to avoid keeping legacy configuration
#### Remove the added configuration and keep the legacy configuration
To remove the configuration:
```diff
framework:
notifier:
chatter_transports:
texter_transports:
- ovhcloud: '%env(OVHCLOUD_DSN)%'
```
In that case, the previous configuration, which was stored under the `chill_main.short_messages.dsn` will be reconfigured into the Notifier component's configuration.
#### Properly configure SMS
You can also properly configure it, as [described in the OVH cloud provider repository](https://github.com/symfony/ovh-cloud-notifier/tree/5.4?tab=readme-ov-file#dsn-example) (where the scheme is `ovhcloud`):
**NOTE**: You have access to all notifier available with the [Notifier component](https://symfony.com/doc/current/notifier.html#notifier-sms-channel). You are not restricted to use OVH as a provider.
```diff
framework:
notifier:
chatter_transports:
texter_transports:
+ ovhcloud: '%env(OVHCLOUD_DSN)%' # this value should be located in a variable, and have `ovhcloud://` as a scheme
chill_main:
- short_messages:
- dsn: '%env(string:SHORT_MESSAGE_DSN)%'
```
## v3.6.0 - 2025-01-16
### Feature
* Importer for addresses does not fails when the postal code is not found with some addresses, and compute a recap list of all addresses that could not be imported. This recap list can be send by email.

View File

@@ -16,8 +16,8 @@
"ext-zlib": "*",
"champs-libres/wopi-bundle": "dev-master@dev",
"champs-libres/wopi-lib": "dev-master@dev",
"doctrine/data-fixtures": "^1.8",
"doctrine/doctrine-bundle": "^2.1",
"doctrine/data-fixtures": "^1.8",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.13.0",
"erusev/parsedown": "^1.7",
@@ -58,9 +58,7 @@
"symfony/messenger": "^5.4",
"symfony/mime": "^5.4",
"symfony/monolog-bundle": "^3.5",
"symfony/notifier": "^5.4",
"symfony/options-resolver": "^5.4",
"symfony/ovh-cloud-notifier": "^5.4",
"symfony/process": "^5.4",
"symfony/property-access": "^5.4",
"symfony/property-info": "^5.4",
@@ -162,9 +160,7 @@
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"php-cs-fixer": "php-cs-fixer fix --config=./.php-cs-fixer.dist.php --show-progress=none",
"phpstan": "phpstan --no-progress",
"rector": "rector --no-progress-bar"
"php-cs-fixer": "php-cs-fixer fix --config=./.php-cs-fixer.dist.php --show-progress=none"
},
"extra": {
"symfony": {

View File

@@ -1,13 +0,0 @@
framework:
notifier:
texter_transports:
#ovhcloud: '%env(OVHCLOUD_DSN)%'
#ovhcloud: '%env(SHORT_MESSAGE_DSN)%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }

View File

@@ -1,19 +0,0 @@
when@dev:
sass_assets:
path: /_dev/assets
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: '@ChillMain/Dev/dev.assets.html.twig'
sass_assets_test1:
path: /_dev/assets_test1
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: '@ChillMain/Dev/dev.assets.test1.html.twig'
sass_assets_test2:
path: /_dev/assets_test2
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: '@ChillMain/Dev/dev.assets.test2.html.twig'

View File

@@ -1,12 +0,0 @@
when@dev:
swagger_ui:
path: /_dev/swagger
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: '@ChillMain/Dev/swagger-ui/index.html.twig'
swagger_specs:
path: /_dev/specs.yaml
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: api/specs.yaml

View File

@@ -12,8 +12,6 @@ This runs eslint **not** taking the baseline into account, thus showing all exis
A script was also added to package.json allowing you to execute ``yarn run eslint``.
This will run eslint, but **taking the baseline into account**, thus only alerting to newly created errors.
The eslint command is configured to also run ``prettier`` which will simply format the code to look more uniform (takes care indentation for example).
Interesting options that can be used in combination with eslint are:
- ``--quiet`` to only get errors and silence the warnings

View File

@@ -60,6 +60,7 @@ Available bundles
* chill docs store: to store documents to people, but also entities,
* chill task: to register task with people,
* chill third party: to register third parties,
* chill family members: to register family members
You will also found the following projects :

View File

@@ -16,7 +16,7 @@
"@eslint/js": "^9.14.0",
"@luminateone/eslint-baseline": "^1.0.9",
"@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node20": "^20.1.4",
"@tsconfig/node14": "^1.0.1",
"@types/dompurify": "^3.0.5",
"@types/eslint__js": "^8.42.3",
"@typescript-eslint/parser": "^8.12.2",
@@ -30,6 +30,7 @@
"eslint-plugin-vue": "^9.30.0",
"fork-awesome": "^1.1.7",
"jquery": "^3.6.0",
"marked": "^12.0.1",
"node-sass": "^8.0.0",
"popper.js": "^1.16.1",
"postcss-loader": "^7.0.2",
@@ -79,12 +80,7 @@
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress",
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> templates/api/specs.yaml",
"specs-validate": "swagger-cli validate templates/api/specs.yaml",
"specs-create-dir": "mkdir -p templates/api",
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
"version": "node --version",
"eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
"eslint": "npx eslint-baseline \"**/*.{js,ts,vue}\""
},
"private": true
}

View File

@@ -20,10 +20,6 @@ return static function (RectorConfig $rectorConfig): void {
__DIR__ . '/src',
]);
$rectorConfig->skip([
\Rector\Php55\Rector\String_\StringClassNameToClassConstantRector::class => __DIR__ . 'src/Bundle/ChillMainBundle/Service/Notifier/LegacyOvhCloudFactory.php'
]);
$rectorConfig->symfonyContainerXml(__DIR__ . '/var/cache/dev/test/App_KernelTestDebugContainer.xml ');
$rectorConfig->symfonyContainerPhp(__DIR__ . '/tests/symfony-container.php');

View File

@@ -15,13 +15,10 @@ use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\NotificationHandlerInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
final readonly class ActivityNotificationHandler implements NotificationHandlerInterface
{
public function __construct(private ActivityRepository $activityRepository, private TranslatableStringHelperInterface $translatableStringHelper) {}
public function __construct(private ActivityRepository $activityRepository) {}
public function getTemplate(Notification $notification, array $options = []): string
{
@@ -40,30 +37,4 @@ final readonly class ActivityNotificationHandler implements NotificationHandlerI
{
return Activity::class === $notification->getRelatedEntityClass();
}
public function getTitle(Notification $notification, array $options = []): TranslatableInterface
{
if (null === $activity = $this->getRelatedEntity($notification)) {
return new TranslatableMessage('activity.deleted');
}
return new TranslatableMessage('activity.title', [
'date' => $activity->getDate(),
'type' => $this->translatableStringHelper->localize($activity->getActivityType()->getName()),
]);
}
public function getAssociatedPersons(Notification $notification, array $options = []): array
{
if (null === $activity = $this->getRelatedEntity($notification)) {
return [];
}
return $activity->getPersonsAssociated();
}
public function getRelatedEntity(Notification $notification): ?Activity
{
return $this->activityRepository->find($notification->getRelatedEntityId());
}
}

View File

@@ -2,7 +2,6 @@ import "es6-promise/auto";
import { createStore } from "vuex";
import { postLocation } from "./api";
import prepareLocations from "./store.locations.js";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
const debug = process.env.NODE_ENV !== "production";
//console.log('window.activity', window.activity);
@@ -24,7 +23,6 @@ const removeIdFromValue = (string, id) => {
const store = createStore({
strict: debug,
state: {
me: null,
activity: window.activity,
socialIssuesOther: [],
socialActionsList: [],
@@ -81,25 +79,15 @@ const store = createStore({
);
},
suggestedUser(state) {
// console.log('current user', state.me)
const existingUserIds = state.activity.users.map((p) => p.id);
let suggestedUsers =
state.activity.activityType.usersVisible === 0
return state.activity.activityType.usersVisible === 0
? []
: [state.activity.accompanyingPeriod.user].filter(
(u) => u !== null && !existingUserIds.includes(u.id),
);
// Add the current user from the state
if (state.me && !existingUserIds.includes(state.me.id)) {
suggestedUsers.push(state.me);
}
console.log("suggested users", suggestedUsers);
return suggestedUsers;
},
suggestedResources(state) {
// const resources = state.activity.accompanyingPeriod.resources;
const resources = state.activity.accompanyingPeriod.resources;
const existingPersonIds = state.activity.persons.map((p) => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map(
(p) => p.id,
@@ -123,9 +111,6 @@ const store = createStore({
},
},
mutations: {
setWhoAmI(state, me) {
state.me = me;
},
// SocialIssueAcc
addIssueInList(state, issue) {
//console.log('add issue list', issue.id);
@@ -341,17 +326,9 @@ const store = createStore({
}
commit("updateLocation", value);
},
getWhoAmI({ commit }) {
const url = `/api/1.0/main/whoami.json`;
makeFetch("GET", url).then((user) => {
commit("setWhoAmI", user);
});
},
},
});
store.dispatch("getWhoAmI");
prepareLocations(store);
export default store;

View File

@@ -1,3 +1,83 @@
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
{% set person_id = null %}
{% if activity.person %}
{% set person_id = activity.person.id %}
{% endif %}
{% set accompanying_course_id = null %}
{% if activity.accompanyingPeriod %}
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
{% endif %}
<div class="item-bloc activity-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
{{ include('@ChillActivity/GenericDoc/activity_document_row.html.twig') }}
<div class="item-row">
<div class="item-col" style="width: unset">
{% if document.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
<div>
{% if activity.accompanyingPeriod is not null and context == 'person' %}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }}
</span>&nbsp;
{% endif %}
<div class="badge-activity-type">
<span class="title_label"></span>
<span class="title_action">
{{ activity.type.name | localize_translatable_string }}
{% if activity.emergency %}
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
{% endif %}
</span>
</div>
</div>
<div class="denomination h2">
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.hasTemplate %}
<div>
<p>{{ document.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
<div class="dates row text-end">
<span>{{ document.createdAt|format_date('short') }}</span>
</div>
</div>
</div>
</div>
<div class="item-row separator">
<div class="item-col item-meta">
{{ mmm.createdBy(document) }}
</div>
<ul class="item-col record_actions flex-shrink-1">
{% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
<li>
{{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false}) }}
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_SEE', activity)%}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_edit', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-edit"></a>
</li>
{% endif %}
</ul>
</div>
</div>

View File

@@ -1,81 +0,0 @@
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
{% set person_id = null %}
{% if activity.person %}
{% set person_id = activity.person.id %}
{% endif %}
{% set accompanying_course_id = null %}
{% if activity.accompanyingPeriod %}
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
{% endif %}
<div class="item-row">
<div class="item-col" style="width: unset">
{% if document.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
<div>
{% if activity.accompanyingPeriod is not null and context == 'person' %}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }}
</span>&nbsp;
{% endif %}
<div class="badge-activity-type">
<span class="title_label"></span>
<span class="title_action">
{{ activity.type.name | localize_translatable_string }}
{% if activity.emergency %}
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
{% endif %}
</span>
</div>
</div>
<div class="denomination h2">
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.hasTemplate %}
<div>
<p>{{ document.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
<div class="dates row text-end">
<span>{{ document.createdAt|format_date('short') }}</span>
</div>
</div>
</div>
</div>
{% if show_actions %}
<div class="item-row separator">
<div class="item-col item-meta">
{{ mmm.createdBy(document) }}
</div>
<ul class="item-col record_actions flex-shrink-1">
{% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
<li>
{{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false}) }}
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_SEE', activity)%}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_edit', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-edit"></a>
</li>
{% endif %}
</ul>
</div>
{% endif %}

View File

@@ -1,54 +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\ActivityBundle\Service\GenericDoc\Normalizer;
use Chill\ActivityBundle\Service\GenericDoc\Providers\AccompanyingPeriodActivityGenericDocProvider;
use Chill\ActivityBundle\Service\GenericDoc\Renderers\AccompanyingPeriodActivityGenericDocRenderer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
final readonly class AccompanyingPeriodActivityGenericDocNormalizer implements GenericDocNormalizerInterface
{
public function __construct(
private StoredObjectRepositoryInterface $storedObjectRepository,
private AccompanyingPeriodActivityGenericDocRenderer $renderer,
private Environment $twig,
private TranslatorInterface $translator,
) {}
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
return AccompanyingPeriodActivityGenericDocProvider::KEY === $genericDocDTO->key
&& 'json' == $format;
}
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
$storedObject = $this->storedObjectRepository->find($genericDocDTO->identifiers['id']);
if (null === $storedObject) {
return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false];
}
return [
'isPresent' => true,
'title' => $storedObject->getTitle(),
'html' => $this->twig->render(
$this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
$this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
),
];
}
}

View File

@@ -13,12 +13,10 @@ namespace Chill\ActivityBundle\Service\GenericDoc\Providers;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepositoryInterface;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -36,47 +34,8 @@ final readonly class AccompanyingPeriodActivityGenericDocProvider implements Gen
private EntityManagerInterface $em,
private Security $security,
private ActivityDocumentACLAwareRepositoryInterface $activityDocumentACLAwareRepository,
private ActivityRepository $activityRepository,
) {}
public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
{
if (null === $activity = $this->getRelatedEntity($genericDocDTO->key, $genericDocDTO->identifiers)) {
return null;
}
return $activity->getDocuments()->findFirst(fn (int $key, StoredObject $storedObject) => $storedObject->getId() === $genericDocDTO->identifiers['id']);
}
public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
{
return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers);
}
public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
{
return self::KEY === $key && array_key_exists('activity_id', $identifiers);
}
private function getRelatedEntity(string $key, array $identifiers): ?Activity
{
return $this->activityRepository->find($identifiers['activity_id']);
}
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
{
if (null === $activity = $this->getRelatedEntity($key, $identifiers)) {
return null;
}
return new GenericDocDTO(
self::KEY,
$identifiers,
\DateTimeImmutable::createFromInterface($activity->getDate()),
$activity->getAccompanyingPeriod(),
);
}
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);

View File

@@ -18,9 +18,6 @@ use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
/**
* @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}>
*/
final readonly class AccompanyingPeriodActivityGenericDocRenderer implements GenericDocRendererInterface
{
public function __construct(private StoredObjectRepository $objectRepository, private ActivityRepository $activityRepository) {}
@@ -32,8 +29,7 @@ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements Gen
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
{
return ($options['row-only'] ?? false) ? '@ChillActivity/GenericDoc/activity_document_row.html.twig' :
'@ChillActivity/GenericDoc/activity_document.html.twig';
return '@ChillActivity/GenericDoc/activity_document.html.twig';
}
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
@@ -42,7 +38,6 @@ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements Gen
'activity' => $this->activityRepository->find($genericDocDTO->identifiers['activity_id']),
'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']),
'context' => $genericDocDTO->getContext(),
'show_actions' => $options['show-actions'] ?? true,
];
}
}

View File

@@ -14,5 +14,3 @@ export:
describe_action_with_subject: >-
Filtré par personne ayant eu un échange entre le {date_from, date} et le {date_to, date}, et un de ces sujets choisis: {reasons}
activity:
title: Échange du {date, date, long} - {type}

View File

@@ -101,7 +101,6 @@ activity:
Insert a document: Insérer un document
Remove a document: Supprimer le document
comment: Commentaire
deleted: Échange supprimé
No documents: Aucun document
# activity filter in list page

View File

@@ -21,7 +21,9 @@ namespace Chill\CalendarBundle\Command;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Service\ShortMessage\ShortMessageTransporterInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonRepository;
use libphonenumber\PhoneNumber;
@@ -34,7 +36,6 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Notifier\TexterInterface;
class SendTestShortMessageOnCalendarCommand extends Command
{
@@ -43,8 +44,9 @@ class SendTestShortMessageOnCalendarCommand extends Command
public function __construct(
private readonly PersonRepository $personRepository,
private readonly PhoneNumberUtil $phoneNumberUtil,
private readonly PhoneNumberHelperInterface $phoneNumberHelper,
private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder,
private readonly TexterInterface $transporter,
private readonly ShortMessageTransporterInterface $transporter,
private readonly UserRepositoryInterface $userRepository,
) {
parent::__construct('chill:calendar:test-send-short-message');
@@ -150,6 +152,10 @@ class SendTestShortMessageOnCalendarCommand extends Command
return $phone;
});
$phone = $helper->ask($input, $output, $question);
$question = new ConfirmationQuestion('really send the message to the phone ?');
$reallySend = (bool) $helper->ask($input, $output, $question);
$messages = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar);
@@ -159,12 +165,8 @@ class SendTestShortMessageOnCalendarCommand extends Command
foreach ($messages as $key => $message) {
$output->writeln("The short message for SMS {$key} will be: ");
$output->writeln($message->getSubject());
$output->writeln('The destination number will be:');
$output->writeln($message->getPhone());
$question = new ConfirmationQuestion('really send the message to the phone ?');
$reallySend = (bool) $helper->ask($input, $output, $question);
$output->writeln($message->getContent());
$message->setPhoneNumber($phone);
if ($reallySend) {
$this->transporter->send($message);

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
@@ -50,21 +49,4 @@ class CalendarDocRepository implements ObjectRepository, CalendarDocRepositoryIn
{
return CalendarDoc::class;
}
/**
* @param StoredObject|int $storedObject the StoredObject instance, or the id of the stored object
*/
public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc
{
$storedObjectId = $storedObject instanceof StoredObject ? $storedObject->getId() : $storedObject;
$qb = $this->repository->createQueryBuilder('c');
$qb->where(
$qb->expr()->eq(':storedObject', 'c.storedObject')
);
$qb->setParameter('storedObject', $storedObjectId);
return $qb->getQuery()->getOneOrNullResult();
}
}

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
interface CalendarDocRepositoryInterface
{
@@ -30,7 +29,5 @@ interface CalendarDocRepositoryInterface
public function findOneBy(array $criteria): ?CalendarDoc;
public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc;
public function getClassName();
}

View File

@@ -106,10 +106,7 @@ export default {
});
state.key = state.key + toAdd.length;
},
addExternals(
state: CalendarRangesState,
externalEvents: (EventInput & { id: string })[],
) {
addExternals(state, externalEvents: (EventInput & { id: string })[]) {
const toAdd = externalEvents.filter(
(r) => !state.rangesIndex.has(r.id),
);
@@ -163,7 +160,7 @@ export default {
state.key = state.key + 1;
}
},
updateRange(state: CalendarRangesState, range: CalendarRange) {
updateRange(state, range: CalendarRange) {
const found = state.ranges.find(
(r) => r.calendarRangeId === range.id && r.is === "range",
);
@@ -210,7 +207,7 @@ export default {
});
},
createRange(
ctx: Context,
ctx,
{
start,
end,
@@ -256,10 +253,10 @@ export default {
throw error;
});
},
deleteRange(ctx: Context, calendarRangeId: number) {
deleteRange(ctx, calendarRangeId: number) {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
makeFetch<undefined, never>("DELETE", url).then(() => {
makeFetch<undefined, never>("DELETE", url).then((_) => {
ctx.commit("removeRange", calendarRangeId);
});
},
@@ -350,10 +347,10 @@ export default {
);
}
return Promise.all(promises).then(() => Promise.resolve(null));
return Promise.all(promises).then((_) => Promise.resolve(null));
},
copyFromWeekToAnotherWeek(
ctx: Context,
ctx,
{ fromMonday, toMonday }: { fromMonday: Date; toMonday: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
@@ -374,7 +371,7 @@ export default {
);
}
return Promise.all(promises).then(() => Promise.resolve(null));
return Promise.all(promises).then((_) => Promise.resolve(null));
},
},
} as Module<CalendarRangesState, State>;

View File

@@ -5,5 +5,71 @@
{% set c = document.calendar %}
<div class="item-bloc">
{{ include('@ChillCalendar/GenericDoc/calendar_document_row.html.twig') }}
<div class="item-row">
<div class="item-col" style="width: unset">
{% if document.storedObject.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.storedObject.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
<div>
{% if c.accompanyingPeriod is not null and context == 'person' %}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ c.accompanyingPeriod.id }}
</span>&nbsp;
{% endif %}
<span class="badge-calendar">
<span class="title_label"></span>
<span class="title_action">
{{ 'Calendar'|trans }}
{% if c.endDate.diff(c.startDate).days >= 1 %}
{{ c.startDate|format_datetime('short', 'short') }}
- {{ c.endDate|format_datetime('short', 'short') }}
{% else %}
{{ c.startDate|format_datetime('short', 'short') }}
- {{ c.endDate|format_datetime('none', 'short') }}
{% endif %}
</span>
</span>
</div>
<div class="denomination h2">
{{ document.storedObject.title|chill_print_or_message("No title") }}
</div>
{% if document.storedObject.hasTemplate %}
<div>
<p>{{ document.storedObject.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
<div class="dates row text-end">
<span>{{ document.storedObject.createdAt|format_date('short') }}</span>
</div>
</div>
</div>
</div>
<div class="item-row separator">
<div class="item-col item-meta">
{{ mmm.createdBy(document) }}
</div>
<ul class="item-col record_actions flex-shrink-1">
{% if is_granted('CHILL_CALENDAR_DOC_SEE', document) %}
<li>
{{ document.storedObject|chill_document_button_group(document.storedObject.title, is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c)) }}
</li>
{% endif %}
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %}
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', {'id': c.id, 'docId': document.id}) }}" class="btn btn-edit"></a>
</li>
{% endif %}
</ul>
</div>
</div>

View File

@@ -1,75 +0,0 @@
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
{% set c = document.calendar %}
<div class="item-row">
<div class="item-col" style="width: unset">
{% if document.storedObject.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.storedObject.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
<div>
{% if c.accompanyingPeriod is not null and context == 'person' %}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ c.accompanyingPeriod.id }}
</span>&nbsp;
{% endif %}
<span class="badge-calendar">
<span class="title_label"></span>
<span class="title_action">
{{ 'Calendar'|trans }}
{% if c.endDate.diff(c.startDate).days >= 1 %}
{{ c.startDate|format_datetime('short', 'short') }}
- {{ c.endDate|format_datetime('short', 'short') }}
{% else %}
{{ c.startDate|format_datetime('short', 'short') }}
- {{ c.endDate|format_datetime('none', 'short') }}
{% endif %}
</span>
</span>
</div>
<div class="denomination h2">
{{ document.storedObject.title|chill_print_or_message("No title") }}
</div>
{% if document.storedObject.hasTemplate %}
<div>
<p>{{ document.storedObject.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
<div class="dates row text-end">
<span>{{ document.storedObject.createdAt|format_date('short') }}</span>
</div>
</div>
</div>
</div>
{% if show_actions %}
<div class="item-row separator">
<div class="item-col item-meta">
{{ mmm.createdBy(document) }}
</div>
<ul class="item-col record_actions flex-shrink-1">
{% if is_granted('CHILL_CALENDAR_DOC_SEE', document) %}
<li>
{{ document.storedObject|chill_document_button_group(document.storedObject.title, is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c)) }}
</li>
{% endif %}
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %}
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', {'id': c.id, 'docId': document.id}) }}" class="btn btn-edit"></a>
</li>
{% endif %}
</ul>
</div>
{% endif %}

View File

@@ -1,51 +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\CalendarBundle\Service\GenericDoc\Normalizer;
use Chill\CalendarBundle\Repository\CalendarDocRepositoryInterface;
use Chill\CalendarBundle\Service\GenericDoc\Providers\AccompanyingPeriodCalendarGenericDocProvider;
use Chill\CalendarBundle\Service\GenericDoc\Renderers\AccompanyingPeriodCalendarGenericDocRenderer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
final readonly class AccompanyingPeriodCalendarGenericDocNormalizer implements GenericDocNormalizerInterface
{
public function __construct(
private AccompanyingPeriodCalendarGenericDocRenderer $renderer,
private CalendarDocRepositoryInterface $calendarDocRepository,
private Environment $twig,
private TranslatorInterface $translator,
) {}
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
return AccompanyingPeriodCalendarGenericDocProvider::KEY === $genericDocDTO->key && 'json' === $format;
}
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
if (null === $calendarDoc = $this->calendarDocRepository->find($genericDocDTO->identifiers['id'])) {
return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false];
}
return [
'isPresent' => true,
'title' => $calendarDoc->getStoredObject()->getTitle(),
'html' => $this->twig->render(
$this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
$this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true])
),
];
}
}

View File

@@ -13,12 +13,10 @@ namespace Chill\CalendarBundle\Service\GenericDoc\Providers;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\CalendarBundle\Repository\CalendarDocRepositoryInterface;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -40,38 +38,8 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen
public function __construct(
private Security $security,
private EntityManagerInterface $em,
private CalendarDocRepositoryInterface $calendarRepository,
) {}
public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
{
return $this->calendarRepository->find($genericDocDTO->identifiers['id'])?->getStoredObject();
}
public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
{
return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers);
}
public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
{
return self::KEY === $key && array_key_exists('id', $identifiers);
}
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
{
if (null === $calendarDoc = $this->calendarRepository->find($identifiers['id'])) {
return null;
}
return new GenericDocDTO(
self::KEY,
$identifiers,
\DateTimeImmutable::createFromInterface($calendarDoc->getCreatedAt() ?? new \DateTimeImmutable('now')),
$calendarDoc->getCalendar()->getAccompanyingPeriod() ?? $calendarDoc->getCalendar()->getPerson()
);
}
/**
* @throws MappingException
*/
@@ -114,7 +82,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen
[Types::INTEGER]
);
return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content);
return $query;
}
public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool

View File

@@ -17,9 +17,6 @@ use Chill\CalendarBundle\Service\GenericDoc\Providers\PersonCalendarGenericDocPr
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
/**
* @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}>
*/
final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements GenericDocRendererInterface
{
public function __construct(private CalendarDocRepository $repository) {}
@@ -31,8 +28,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements Gen
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
{
return $options['row-only'] ?? false ? '@ChillCalendar/GenericDoc/calendar_document_row.html.twig'
: '@ChillCalendar/GenericDoc/calendar_document.html.twig';
return '@ChillCalendar/GenericDoc/calendar_document.html.twig';
}
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
@@ -40,7 +36,6 @@ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements Gen
return [
'document' => $this->repository->find($genericDocDTO->identifiers['id']),
'context' => $genericDocDTO->getContext(),
'show_actions' => $options['show-actions'] ?? true,
];
}
}

View File

@@ -21,17 +21,11 @@ namespace Chill\CalendarBundle\Service\ShortMessageNotification;
use Chill\CalendarBundle\Entity\Calendar;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Notifier\TexterInterface;
use Symfony\Component\Messenger\MessageBusInterface;
class BulkCalendarShortMessageSender
{
public function __construct(
private readonly CalendarForShortMessageProvider $provider,
private readonly EntityManagerInterface $em,
private readonly LoggerInterface $logger,
private readonly TexterInterface $texter,
private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder,
) {}
public function __construct(private readonly CalendarForShortMessageProvider $provider, private readonly EntityManagerInterface $em, private readonly LoggerInterface $logger, private readonly MessageBusInterface $messageBus, private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder) {}
public function sendBulkMessageToEligibleCalendars()
{
@@ -42,7 +36,7 @@ class BulkCalendarShortMessageSender
$smses = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar);
foreach ($smses as $sms) {
$this->texter->send($sms);
$this->messageBus->dispatch($sms);
++$countSms;
}

View File

@@ -19,26 +19,12 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
use Chill\CalendarBundle\Entity\Calendar;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
use Symfony\Component\Notifier\Message\SmsMessage;
use Chill\MainBundle\Service\ShortMessage\ShortMessage;
class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBuilderInterface
{
private readonly PhoneNumberUtil $phoneUtil;
public function __construct(private readonly \Twig\Environment $engine) {}
public function __construct(private readonly \Twig\Environment $engine)
{
$this->phoneUtil = PhoneNumberUtil::getInstance();
}
/**
* @return list<SmsMessage>
*
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function buildMessageForCalendar(Calendar $calendar): array
{
if (true !== $calendar->getSendSMS()) {
@@ -53,14 +39,16 @@ class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBu
}
if (Calendar::SMS_PENDING === $calendar->getSmsStatus()) {
$toUsers[] = new SmsMessage(
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
$toUsers[] = new ShortMessage(
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]),
$person->getMobilenumber(),
ShortMessage::PRIORITY_LOW
);
} elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) {
$toUsers[] = new SmsMessage(
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
$toUsers[] = new ShortMessage(
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]),
$person->getMobilenumber(),
ShortMessage::PRIORITY_LOW
);
}
}

View File

@@ -19,12 +19,12 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
use Chill\CalendarBundle\Entity\Calendar;
use Symfony\Component\Notifier\Message\SmsMessage;
use Chill\MainBundle\Service\ShortMessage\ShortMessage;
interface ShortMessageForCalendarBuilderInterface
{
/**
* @return list<SmsMessage>
* @return array|ShortMessage[]
*/
public function buildMessageForCalendar(Calendar $calendar): array;
}

View File

@@ -23,16 +23,17 @@ use Chill\CalendarBundle\Service\ShortMessageNotification\BulkCalendarShortMessa
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Service\ShortMessage\ShortMessage;
use Chill\MainBundle\Test\PrepareUserTrait;
use Chill\PersonBundle\DataFixtures\Helper\PersonRandomHelper;
use Doctrine\ORM\EntityManagerInterface;
use libphonenumber\PhoneNumberUtil;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\TexterInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* @internal
@@ -100,23 +101,24 @@ final class BulkCalendarShortMessageSenderTest extends KernelTestCase
$messageBuilder->buildMessageForCalendar(Argument::type(Calendar::class))
->willReturn(
[
new SmsMessage(
'+32470123456',
new ShortMessage(
'content',
PhoneNumberUtil::getInstance()->parse('+32470123456', 'BE'),
ShortMessage::PRIORITY_MEDIUM
),
]
);
$texter = $this->prophesize(TexterInterface::class);
$texter->send(Argument::type(SmsMessage::class))
->will(fn ($args): SentMessage => new SentMessage($args[0], 'sms'))
$bus = $this->prophesize(MessageBusInterface::class);
$bus->dispatch(Argument::type(ShortMessage::class))
->willReturn(new Envelope(new \stdClass()))
->shouldBeCalledTimes(1);
$bulk = new BulkCalendarShortMessageSender(
$provider->reveal(),
$em,
new NullLogger(),
$texter->reveal(),
$bus->reveal(),
$messageBuilder->reveal()
);

View File

@@ -23,6 +23,7 @@ use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultShortMessageFor
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\Person;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
@@ -89,9 +90,10 @@ final class DefaultShortMessageForCalendarBuilderTest extends TestCase
$this->assertCount(1, $sms);
$this->assertEquals(
'+32470123456',
$sms[0]->getPhone()
$this->phoneNumberUtil->format($sms[0]->getPhoneNumber(), PhoneNumberFormat::E164)
);
$this->assertEquals('message content', $sms[0]->getSubject());
$this->assertEquals('message content', $sms[0]->getContent());
$this->assertEquals('low', $sms[0]->getPriority());
// if the calendar is canceled
$calendar
@@ -103,8 +105,9 @@ final class DefaultShortMessageForCalendarBuilderTest extends TestCase
$this->assertCount(1, $sms);
$this->assertEquals(
'+32470123456',
$sms[0]->getRecipientId(),
$this->phoneNumberUtil->format($sms[0]->getPhoneNumber(), PhoneNumberFormat::E164)
);
$this->assertEquals('message canceled', $sms[0]->getSubject());
$this->assertEquals('message canceled', $sms[0]->getContent());
$this->assertEquals('low', $sms[0]->getPriority());
}
}

View File

@@ -1,82 +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\DocGeneratorBundle\Tests\Controller;
use Chill\DocStoreBundle\Controller\GenericDocForAccompanyingPeriodListApiController;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\MainBundle\Pagination\Paginator;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
/**
* @internal
*
* @coversNothing
*/
class GenericDocForAccompanyingPeriodListApiControllerTest extends TestCase
{
public function testSmokeTest(): void
{
$accompanyingPeriod = new AccompanyingPeriod();
$docs = [
new GenericDocDTO('dummy', ['id' => 9], new \DateTimeImmutable('2024-08-01'), $accompanyingPeriod),
new GenericDocDTO('dummy', ['id' => 1], new \DateTimeImmutable('2024-09-01'), $accompanyingPeriod),
];
$manager = $this->createMock(ManagerInterface::class);
$manager->method('findDocForAccompanyingPeriod')->with($accompanyingPeriod)->willReturn($docs);
$manager->method('countDocForAccompanyingPeriod')->with($accompanyingPeriod)->willReturn(2);
$paginatorFactory = $this->createMock(PaginatorFactoryInterface::class);
$paginatorFactory->method('create')->with(2)->willReturn(new Paginator(
2,
20,
1,
'/route',
[],
$this->createMock(UrlGeneratorInterface::class),
'page',
'item-per-page'
));
$serializer = $this->createMock(SerializerInterface::class);
$serializer->method('serialize')->with($this->isInstanceOf(Collection::class))->willReturn(
json_encode(['docs' => []])
);
$security = $this->createMock(Security::class);
$security->expects($this->once())->method('isGranted')
->with(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)->willReturn(true);
$controller = new GenericDocForAccompanyingPeriodListApiController(
$manager,
$security,
$paginatorFactory,
$serializer,
);
$response = $controller($accompanyingPeriod);
$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertEquals('{"docs":[]}', $response->getContent());
}
}

View File

@@ -14,7 +14,6 @@ namespace Chill\DocStoreBundle;
use Chill\DocStoreBundle\DependencyInjection\Compiler\StorageConfigurationCompilerPass;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -29,8 +28,6 @@ class ChillDocStoreBundle extends Bundle
->addTag('chill_doc_store.generic_doc_person_provider');
$container->registerForAutoconfiguration(GenericDocRendererInterface::class)
->addTag('chill_doc_store.generic_doc_renderer');
$container->registerForAutoconfiguration(GenericDocNormalizerInterface::class)
->addTag('chill_doc_store.generic_doc_metadata_normalizer');
$container->addCompilerPass(new StorageConfigurationCompilerPass());
}

View File

@@ -92,14 +92,13 @@ class DocumentCategoryController extends AbstractController
$nextId = $em
->createQuery(
'SELECT (CASE WHEN MAX(c.idInsideBundle) IS NULL THEN 1 ELSE MAX(c.idInsideBundle) + 1 END)
FROM ChillDocStoreBundle:DocumentCategory c'
'SELECT MAX(c.idInsideBundle) + 1 FROM ChillDocStoreBundle:DocumentCategory c'
)
->getSingleScalarResult();
->getSingleResult();
$documentCategory = new DocumentCategory(
ChillDocStoreBundle::class,
$nextId
reset($nextId)
);
$documentCategory

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
use Chill\DocStoreBundle\GenericDoc\Manager;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
@@ -25,7 +25,7 @@ final readonly class GenericDocForAccompanyingPeriodController
{
public function __construct(
private FilterOrderHelperFactory $filterOrderHelperFactory,
private ManagerInterface $manager,
private Manager $manager,
private PaginatorFactory $paginator,
private Security $security,
private \Twig\Environment $twig,
@@ -68,9 +68,6 @@ final readonly class GenericDocForAccompanyingPeriodController
);
$paginator = $this->paginator->create($nb);
// restrict the number of items for performance reasons
$paginator->setItemsPerPage(20);
$documents = $this->manager->findDocForAccompanyingPeriod(
$accompanyingPeriod,
$paginator->getCurrentPageFirstItemNumber(),

View File

@@ -1,57 +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\DocStoreBundle\Controller;
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Provide the list of GenericDoc for an accompanying period.
*/
final readonly class GenericDocForAccompanyingPeriodListApiController
{
public function __construct(
private ManagerInterface $manager,
private Security $security,
private PaginatorFactoryInterface $paginator,
private SerializerInterface $serializer,
) {}
#[Route('/api/1.0/doc-store/generic-doc/by-period/{id}/index', methods: ['GET'])]
public function __invoke(AccompanyingPeriod $accompanyingPeriod): JsonResponse
{
if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)) {
throw new AccessDeniedHttpException('not allowed to see the documents for accompanying period');
}
$nb = $this->manager->countDocForAccompanyingPeriod($accompanyingPeriod);
$paginator = $this->paginator->create($nb);
$docs = $this->manager->findDocForAccompanyingPeriod($accompanyingPeriod, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage());
$collection = new Collection($docs, $paginator);
return new JsonResponse(
$this->serializer->serialize($collection, 'json', [AbstractNormalizer::GROUPS => ['read']]),
json: true,
);
}
}

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
use Chill\DocStoreBundle\GenericDoc\Manager;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
@@ -25,7 +25,7 @@ final readonly class GenericDocForPerson
{
public function __construct(
private FilterOrderHelperFactory $filterOrderHelperFactory,
private ManagerInterface $manager,
private Manager $manager,
private PaginatorFactory $paginator,
private Security $security,
private \Twig\Environment $twig,

View File

@@ -46,10 +46,9 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
#[ORM\ManyToOne(targetEntity: DocGeneratorTemplate::class)]
private ?DocGeneratorTemplate $template = null;
/**
* Store the title of the document, if the title is set before the document.
*/
private string $proxyTitle = '';
#[Assert\Length(min: 2, max: 250)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
private string $title = '';
#[ORM\ManyToOne(targetEntity: \Chill\MainBundle\Entity\User::class)]
private ?\Chill\MainBundle\Entity\User $user = null;
@@ -79,10 +78,9 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
return $this->template;
}
#[Assert\Length(min: 2, max: 250)]
public function getTitle(): string
{
return (string) $this->getObject()?->getTitle();
return $this->title;
}
public function getUser()
@@ -115,10 +113,6 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
{
$this->object = $object;
if ('' !== $this->proxyTitle) {
$this->object->setTitle($this->proxyTitle);
}
return $this;
}
@@ -131,11 +125,7 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
public function setTitle(string $title): self
{
if (null !== $this->getObject()) {
$this->getObject()->setTitle($title);
} else {
$this->proxyTitle = $title;
}
$this->title = $title;
return $this;
}

View File

@@ -1,20 +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\DocStoreBundle\GenericDoc\Exception;
class AssociatedStoredObjectNotFound extends \RuntimeException
{
public function __construct(string $key, array $identifiers, int $code = 0, ?\Throwable $previous = null)
{
parent::__construct(sprintf('No stored object found for generic doc with key "%s" and identifiers "%s"', $key, json_encode($identifiers)), $code, $previous);
}
}

View File

@@ -1,14 +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\DocStoreBundle\GenericDoc\Exception;
class NotNormalizableGenericDocException extends \LogicException {}

View File

@@ -1,14 +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\DocStoreBundle\GenericDoc\Exception;
class UnexpectedValueException extends \UnexpectedValueException {}

View File

@@ -13,7 +13,7 @@ namespace Chill\DocStoreBundle\GenericDoc;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
interface GenericDocForAccompanyingPeriodProviderInterface extends GenericDocProviderInterface
interface GenericDocForAccompanyingPeriodProviderInterface
{
public function buildFetchQueryForAccompanyingPeriod(
AccompanyingPeriod $accompanyingPeriod,

View File

@@ -1,30 +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\DocStoreBundle\GenericDoc;
/**
* Normalize a Generic Doc.
*/
interface GenericDocNormalizerInterface
{
/**
* Return true if a generic doc can be normalized by this implementation.
*/
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool;
/**
* Normalize a generic doc into an array.
*
* @return array{title: string, html?: string, isPresent: bool}
*/
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array;
}

View File

@@ -1,38 +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\DocStoreBundle\GenericDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
interface GenericDocProviderInterface
{
public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject;
/**
* Return true if this provider supports the given Generic doc for various informations.
*
* Concerned:
*
* - @see{self::fetchAssociatedStoredObject}
*/
public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool;
/**
* return true if the implementation supports key and identifiers.
*/
public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool;
/**
* Build a GenericDocDTO, given the key and identifiers.
*/
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO;
}

View File

@@ -11,16 +11,13 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\GenericDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\Exception\AssociatedStoredObjectNotFound;
use Chill\DocStoreBundle\GenericDoc\Exception\NotNormalizableGenericDocException;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Types\Types;
final readonly class Manager implements ManagerInterface
final readonly class Manager
{
private FetchQueryToSqlBuilder $builder;
@@ -34,16 +31,16 @@ final readonly class Manager implements ManagerInterface
* @var iterable<GenericDocForPersonProviderInterface>
*/
private iterable $providersForPerson,
/**
* @var iterable<GenericDocNormalizerInterface>
*/
private iterable $genericDocNormalizers,
private Connection $connection,
) {
$this->builder = new FetchQueryToSqlBuilder();
}
/**
* @param list<string> $places
*
* @throws Exception
*/
public function countDocForAccompanyingPeriod(
AccompanyingPeriod $accompanyingPeriod,
?\DateTimeImmutable $startDate = null,
@@ -86,6 +83,13 @@ final readonly class Manager implements ManagerInterface
return $this->countDoc($sql, $params, $types);
}
/**
* @param list<string> $places places to search. When empty, search in all places
*
* @return iterable<GenericDocDTO>
*
* @throws Exception
*/
public function findDocForAccompanyingPeriod(
AccompanyingPeriod $accompanyingPeriod,
int $offset = 0,
@@ -125,35 +129,10 @@ final readonly class Manager implements ManagerInterface
}
/**
* Fetch a generic doc, if it does exists.
* @param list<string> $places places to search. When empty, search in all places
*
* Currently implemented only on generic docs linked with accompanying period
* @return iterable<GenericDocDTO>
*/
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
{
foreach ($this->providersForAccompanyingPeriod as $provider) {
if ($provider->supportsKeyAndIdentifiers($key, $identifiers)) {
return $provider->buildOneGenericDoc($key, $identifiers);
}
}
return null;
}
/**
* @throws AssociatedStoredObjectNotFound if no stored object can be found
*/
public function fetchStoredObject(GenericDocDTO $genericDocDTO): StoredObject
{
foreach ($this->providersForAccompanyingPeriod as $provider) {
if ($provider->supportsGenericDoc($genericDocDTO)) {
return $provider->fetchAssociatedStoredObject($genericDocDTO);
}
}
throw new AssociatedStoredObjectNotFound($genericDocDTO->key, $genericDocDTO->identifiers);
}
public function findDocForPerson(
Person $person,
int $offset = 0,
@@ -182,28 +161,6 @@ final readonly class Manager implements ManagerInterface
return $this->places($sql, $params, $types);
}
public function isGenericDocNormalizable(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
foreach ($this->genericDocNormalizers as $genericDocNormalizer) {
if ($genericDocNormalizer->supportsNormalization($genericDocDTO, $format, $context)) {
return true;
}
}
return false;
}
public function normalizeGenericDoc(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
foreach ($this->genericDocNormalizers as $genericDocNormalizer) {
if ($genericDocNormalizer->supportsNormalization($genericDocDTO, $format, $context)) {
return $genericDocNormalizer->normalize($genericDocDTO, $format, $context);
}
}
throw new NotNormalizableGenericDocException();
}
private function places(string $sql, array $params, array $types): array
{
if ('' === $sql) {

View File

@@ -1,64 +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\DocStoreBundle\GenericDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\Exception\AssociatedStoredObjectNotFound;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\DBAL\Exception;
interface ManagerInterface
{
/**
* @param list<string> $places
*
* @throws Exception
*/
public function countDocForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): int;
public function countDocForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): int;
/**
* @param list<string> $places places to search. When empty, search in all places
*
* @return iterable<GenericDocDTO>
*
* @throws Exception
*/
public function findDocForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, int $offset = 0, int $limit = 20, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): iterable;
/**
* @param list<string> $places places to search. When empty, search in all places
*
* @return iterable<GenericDocDTO>
*/
public function findDocForPerson(Person $person, int $offset = 0, int $limit = 20, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): iterable;
public function placesForPerson(Person $person): array;
public function placesForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): array;
public function isGenericDocNormalizable(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool;
/**
* @return array{title: string, html?: string}
*/
public function normalizeGenericDoc(GenericDocDTO $genericDocDTO, string $format, array $context = []): array;
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO;
/**
* @throws AssociatedStoredObjectNotFound if no stored object can be found
*/
public function fetchStoredObject(GenericDocDTO $genericDocDTO): StoredObject;
}

View File

@@ -1,56 +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\DocStoreBundle\GenericDoc\Normalizer;
use Chill\DocStoreBundle\GenericDoc\Exception\UnexpectedValueException;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericDocProvider;
use Chill\DocStoreBundle\GenericDoc\Renderer\AccompanyingCourseDocumentGenericDocRenderer;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Twig\Environment;
class AccompanyingCourseDocumentGenericDocNormalizer implements GenericDocNormalizerInterface
{
public function __construct(
private readonly AccompanyingCourseDocumentRepository $repository,
private readonly Environment $twig,
private readonly AccompanyingCourseDocumentGenericDocRenderer $renderer,
) {}
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
return AccompanyingCourseDocumentGenericDocProvider::KEY === $genericDocDTO->key;
}
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
if (!array_key_exists('id', $genericDocDTO->identifiers)) {
throw new UnexpectedValueException('key id not found in identifier');
}
$document = $this->repository->find($genericDocDTO->identifiers['id']);
if (null === $document) {
throw new UnexpectedValueException('document not found with id '.$genericDocDTO->identifiers['id']);
}
return [
'isPresent' => true,
'title' => $document->getTitle(),
'html' => $this->twig->render(
$this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
$this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true])
),
];
}
}

View File

@@ -1,51 +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\DocStoreBundle\GenericDoc\Normalizer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
use Chill\DocStoreBundle\GenericDoc\Renderer\AccompanyingCourseDocumentGenericDocRenderer;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
final readonly class PersonDocumentGenericDocNormalizer implements GenericDocNormalizerInterface
{
public function __construct(
private PersonDocumentRepository $personDocumentRepository,
private AccompanyingCourseDocumentGenericDocRenderer $renderer,
private Environment $twig,
private TranslatorInterface $translator,
) {}
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
return PersonDocumentGenericDocProvider::KEY === $genericDocDTO->key && 'json' === $format;
}
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
if (null === $personDocument = $this->personDocumentRepository->find($genericDocDTO->identifiers['id'])) {
return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false];
}
return [
'isPresent' => true,
'title' => $personDocument->getTitle(),
'html' => $this->twig->render(
$this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
$this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true])
),
];
}
}

View File

@@ -12,13 +12,10 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\GenericDoc\Providers;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
@@ -34,47 +31,17 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
public function __construct(
private Security $security,
private EntityManagerInterface $entityManager,
private AccompanyingCourseDocumentRepository $accompanyingCourseDocumentRepository,
) {}
public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
{
return $this->accompanyingCourseDocumentRepository->find($genericDocDTO->identifiers['id'])?->getObject();
}
public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
{
return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers);
}
public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
{
return self::KEY === $key;
}
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
{
if (null === $accompanyingCourseDocument = $this->accompanyingCourseDocumentRepository->find($identifiers['id'])) {
return null;
}
return new GenericDocDTO(
self::KEY,
$identifiers,
\DateTimeImmutable::createFromInterface($accompanyingCourseDocument->getDate()),
$accompanyingCourseDocument->getCourse(),
);
}
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
$classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
$query = new FetchQuery(
self::KEY,
sprintf('jsonb_build_object(\'id\', acc_course_document.%s)', $classMetadata->getIdentifierColumnNames()[0]),
sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]),
$classMetadata->getColumnName('date'),
$classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS acc_course_document'
$classMetadata->getSchemaName().'.'.$classMetadata->getTableName()
);
$query->addWhereClause(
@@ -97,7 +64,7 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
$query = new FetchQuery(
self::KEY,
sprintf('jsonb_build_object(\'id\', acc_course_document.%s)', $classMetadata->getIdentifierColumnNames()[0]),
sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]),
$classMetadata->getColumnName('date'),
$classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS acc_course_document'
);
@@ -143,7 +110,6 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
private function addWhereClause(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{
$classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
$storedObjectMetadata = $this->entityManager->getClassMetadata(StoredObject::class);
if (null !== $startDate) {
$query->addWhereClause(
@@ -162,19 +128,9 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
}
if (null !== $content and '' !== $content) {
// add join clause to stored_object table
$query->addJoinClause(
sprintf(
'JOIN %s AS doc_store ON doc_store.%s = acc_course_document.%s',
$storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(),
$storedObjectMetadata->getSingleIdentifierColumnName(),
$classMetadata->getSingleAssociationJoinColumnName('object')
)
);
$query->addWhereClause(
sprintf(
'(doc_store.%s ilike ? OR acc_course_document.%s ilike ?)',
'(%s ilike ? OR %s ilike ?)',
$classMetadata->getColumnName('title'),
$classMetadata->getColumnName('description')
),

View File

@@ -11,13 +11,10 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\GenericDoc\Providers;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
@@ -30,38 +27,8 @@ final readonly class PersonDocumentGenericDocProvider implements GenericDocForPe
public function __construct(
private Security $security,
private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository,
private PersonDocumentRepository $personDocumentRepository,
) {}
public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
{
return $this->personDocumentRepository->find($genericDocDTO->identifiers['id'])?->getObject();
}
public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
{
return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers);
}
public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
{
return self::KEY === $key && array_key_exists('id', $identifiers);
}
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
{
if (null === $document = $this->personDocumentRepository->find($identifiers['id'])) {
return null;
}
return new GenericDocDTO(
self::KEY,
$identifiers,
\DateTimeImmutable::createFromInterface($document->getDate()),
$document->getPerson()
);
}
public function buildFetchQueryForPerson(
Person $person,
?\DateTimeImmutable $startDate = null,

View File

@@ -18,9 +18,6 @@ use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericD
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
/**
* @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}>
*/
final readonly class AccompanyingCourseDocumentGenericDocRenderer implements GenericDocRendererInterface
{
public function __construct(
@@ -36,10 +33,6 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
{
if ($options['row-only'] ?? false) {
return '@ChillDocStore/List/list_item_row.html.twig';
}
return '@ChillDocStore/List/list_item.html.twig';
}
@@ -51,7 +44,6 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen
'accompanyingCourse' => $doc->getCourse(),
'options' => $options,
'context' => $genericDocDTO->getContext(),
'show_actions' => $options['show-actions'] ?? true,
];
}
@@ -61,7 +53,6 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen
'person' => $doc->getPerson(),
'options' => $options,
'context' => $genericDocDTO->getContext(),
'show_actions' => $options['show-actions'] ?? true,
];
}
}

View File

@@ -13,25 +13,11 @@ namespace Chill\DocStoreBundle\GenericDoc\Twig;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
/**
* Render a generic doc, to display it into a page.
*
* @template T of array
*/
interface GenericDocRendererInterface
{
/**
* @param T $options the options defined by the renderer
*/
public function supports(GenericDocDTO $genericDocDTO, $options = []): bool;
/**
* @param T $options the options defined by the renderer
*/
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string;
/**
* @param T $options the options defined by the renderer
*/
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array;
}

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
@@ -137,7 +136,6 @@ final readonly class PersonDocumentACLAwareRepository implements PersonDocumentA
private function addFilterClauses(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{
$personDocMetadata = $this->em->getClassMetadata(PersonDocument::class);
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
if (null !== $startDate) {
$query->addWhereClause(
@@ -156,20 +154,10 @@ final readonly class PersonDocumentACLAwareRepository implements PersonDocumentA
}
if (null !== $content and '' !== $content) {
$query->addJoinClause(
sprintf(
'JOIN %s AS doc_store ON doc_store.%s = person_document.%s',
$storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(),
$storedObjectMetadata->getSingleIdentifierColumnName(),
$personDocMetadata->getSingleAssociationJoinColumnName('object')
)
);
$query->addWhereClause(
sprintf(
'(doc_store.%s ilike ? OR person_document.%s ilike ?)',
$storedObjectMetadata->getColumnName('title'),
'(%s ilike ? OR %s ilike ?)',
$personDocMetadata->getColumnName('title'),
$personDocMetadata->getColumnName('description')
),
['%'.$content.'%', '%'.$content.'%'],

View File

@@ -1,10 +0,0 @@
import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
export function fetch_generic_docs_by_accompanying_period(
periodId: number,
): Promise<GenericDocForAccompanyingPeriod[]> {
return fetchResults(
`/api/1.0/doc-store/generic-doc/by-period/${periodId}/index`,
);
}

View File

@@ -1,4 +1,4 @@
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
import { _createI18n } from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
import { createApp } from "vue";
import { StoredObject, StoredObjectStatusChange } from "../../types";

View File

@@ -1,5 +1,8 @@
import { DateTime, User } from "ChillMainAssets/types";
import { SignedUrlGet } from "ChillDocStoreAssets/vuejs/StoredObjectButton/helpers";
import {
DateTime,
User,
} from "../../../ChillMainBundle/Resources/public/types";
import { SignedUrlGet } from "./vuejs/StoredObjectButton/helpers";
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
@@ -135,10 +138,3 @@ export interface ZoomLevel {
nl?: string;
};
}
export interface GenericDoc {
type: "doc_store_generic_doc";
key: string;
context: "person" | "accompanying-period";
doc_date: DateTime;
}

View File

@@ -1,71 +0,0 @@
import { DateTime } from "ChillMainAssets/types";
import { StoredObject } from "ChillDocStoreAssets/types/index";
export interface GenericDocMetadata {
isPresent: boolean;
}
/**
* Empty metadata for a GenericDoc
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface EmptyMetadata extends GenericDocMetadata {}
/**
* Minimal Metadata for a GenericDoc with a normalizer
*/
export interface BaseMetadata extends GenericDocMetadata {
title: string;
}
/**
* A generic doc is a document attached to a Person or an AccompanyingPeriod.
*/
export interface GenericDoc {
type: "doc_store_generic_doc";
uniqueKey: string;
key: string;
identifiers: object;
context: "person" | "accompanying-period";
doc_date: DateTime;
metadata: GenericDocMetadata;
storedObject: StoredObject | null;
}
export interface GenericDocForAccompanyingPeriod extends GenericDoc {
context: "accompanying-period";
}
interface BaseMetadataWithHtml extends BaseMetadata {
html: string;
}
export interface GenericDocForAccompanyingCourseDocument
extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_document";
metadata: BaseMetadataWithHtml;
}
export interface GenericDocForAccompanyingCourseActivityDocument
extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_activity_document";
metadata: BaseMetadataWithHtml;
}
export interface GenericDocForAccompanyingCourseCalendarDocument
extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_calendar_document";
metadata: BaseMetadataWithHtml;
}
export interface GenericDocForAccompanyingCoursePersonDocument
extends GenericDocForAccompanyingPeriod {
key: "person_document";
metadata: BaseMetadataWithHtml;
}
export interface GenericDocForAccompanyingCourseWorkEvaluationDocument
extends GenericDocForAccompanyingPeriod {
key: "accompanying_period_work_evaluation_document";
metadata: BaseMetadataWithHtml;
}

View File

@@ -66,7 +66,7 @@ const open_button = ref<HTMLAnchorElement | null>(null);
function buildDocumentName(): string {
let document_name = props.filename ?? props.storedObject.title;
if ("" === document_name || null === document_name) {
if ("" === document_name) {
document_name = "document";
}

View File

@@ -1,3 +1,120 @@
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
<div class="item-bloc">
{% include '@ChillDocStore/List/list_item_row.html.twig'%}
<div class="item-row">
<div class="item-col" style="width: unset">
{% if document.object.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.object.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.object.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
{% if context == 'person' and accompanyingCourse is defined %}
<div>
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ accompanyingCourse.id }}
</span>&nbsp;
</div>
{% elseif context == 'accompanying-period' and person is defined %}
<div>
<span class="badge bg-primary">
{{ 'Document from person %name%'|trans({ '%name%': document.person|chill_entity_render_string }) }}
</span>&nbsp;
</div>
{% endif %}
<div class="denomination h2">
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.object.type is not empty %}
<div>
{{ mm.mimeIcon(document.object.type) }}
</div>
{% endif %}
<div>
<p>{{ document.category.name|localize_translatable_string }}</p>
</div>
{% if document.object.hasTemplate %}
<div>
<p>{{ document.object.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
{% if document.date is not null %}
<div class="dates row text-end">
<span>{{ document.date|format_date('short') }}</span>
</div>
{% endif %}
</div>
</div>
</div>
{% if document.description is not empty %}
<div class="item-row">
<blockquote class="chill-user-quote col">
{{ document.description|chill_markdown_to_html }}
</blockquote>
</div>
{% endif %}
<div class="item-row separator">
<div class="item-col item-meta">
{{ mmm.createdBy(document) }}
</div>
<ul class="item-col record_actions flex-shrink-1">
{% if document.course is defined %}
<li>
{{ chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) }}
</li>
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
<li>
{{ document.object|chill_document_button_group(document.title) }}
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
<li class="delete">
<a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', document.course) %}
<li>
<a href="{{ chill_path_add_return_path('chill_doc_store_accompanying_course_document_duplicate', {'id': document.id}) }}" class="btn btn-duplicate" title="{{ 'Duplicate'|trans|e('html_attr') }}"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
<li>
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
<li>
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% else %}
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
<li>
{{ document.object|chill_document_button_group(document.title) }}
</li>
<li>
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %}
<li>
<a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a>
</li>
{% endif %}
{% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %}
<li class="delete">
<a href="{{ chill_return_path_or('chill_docstore_person_document_delete', {'person': person.id, 'id': document.id}) }}" class="btn btn-delete"></a>
</li>
{% endif %}
{% endif %}
</ul>
</div>
</div>

View File

@@ -1,119 +0,0 @@
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
<div class="item-row">
<div class="item-col" style="width: unset">
{% if document.object.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.object.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.object.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
{% if context == 'person' and accompanyingCourse is defined %}
<div>
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ accompanyingCourse.id }}
</span>&nbsp;
</div>
{% elseif context == 'accompanying-period' and person is defined %}
<div>
<span class="badge bg-primary">
{{ 'Document from person %name%'|trans({ '%name%': document.person|chill_entity_render_string }) }}
</span>&nbsp;
</div>
{% endif %}
<div class="denomination h2">
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.object.type is not empty %}
<div>
{{ mm.mimeIcon(document.object.type) }}
</div>
{% endif %}
<div>
<p>{{ document.category.name|localize_translatable_string }}</p>
</div>
{% if document.object.hasTemplate %}
<div>
<p>{{ document.object.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
{% if document.date is not null %}
<div class="dates row text-end">
<span>{{ document.date|format_date('short') }}</span>
</div>
{% endif %}
</div>
</div>
</div>
{% if document.description is not empty %}
<div class="item-row">
<blockquote class="chill-user-quote col">
{{ document.description|chill_markdown_to_html }}
</blockquote>
</div>
{% endif %}
{% if show_actions %}
<div class="item-row separator">
<div class="item-col item-meta">
{{ mmm.createdBy(document) }}
</div>
<ul class="item-col record_actions flex-shrink-1">
{% if document.course is defined %}
<li>
{{ chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) }}
</li>
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
<li>
{{ document.object|chill_document_button_group(document.title) }}
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
<li class="delete">
<a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', document.course) %}
<li>
<a href="{{ chill_path_add_return_path('chill_doc_store_accompanying_course_document_duplicate', {'id': document.id}) }}" class="btn btn-duplicate" title="{{ 'Duplicate'|trans|e('html_attr') }}"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
<li>
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
<li>
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% else %}
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
<li>
{{ document.object|chill_document_button_group(document.title) }}
</li>
<li>
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %}
<li>
<a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a>
</li>
{% endif %}
{% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %}
<li class="delete">
<a href="{{ chill_return_path_or('chill_docstore_person_document_delete', {'person': person.id, 'id': document.id}) }}" class="btn btn-delete"></a>
</li>
{% endif %}
{% endif %}
</ul>
</div>
{% endif %}

View File

@@ -24,9 +24,9 @@
{% endif %}
{% endif %}
<div class="row g-3">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4">
<div class="card">
<div class="card"">
<div class="card-body">
<h2 class="card-title">{{ title }}</h2>
<h3>{{ 'workflow.public_link.main_document'|trans }}</h3>
@@ -39,21 +39,5 @@
</div>
</div>
</div>
{% for attachment in attachments %}
<div class="col-xs-12 col-sm-6 col-md-4">
<div class="card">
<div class="card-body">
<h2 class="card-title">{{ attachment.proxyStoredObject.title }}</h2>
<h3>{{ 'workflow.public_link.attachment'|trans }}</h3>
<ul class="record_actions slim small">
<li>
{{ attachment.proxyStoredObject|chill_document_download_only_button(storedObject.title(), false) }}
</li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -12,8 +12,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Security\Authorization;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
@@ -28,12 +26,7 @@ class StoredObjectVoter extends Voter
{
public const LOG_PREFIX = '[stored object voter] ';
public function __construct(
private readonly Security $security,
private readonly iterable $storedObjectVoters,
private readonly LoggerInterface $logger,
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
) {}
public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters, private readonly LoggerInterface $logger) {}
protected function supports($attribute, $subject): bool
{
@@ -46,16 +39,6 @@ class StoredObjectVoter extends Voter
/** @var StoredObject $subject */
$attributeAsEnum = StoredObjectRoleEnum::from($attribute);
// check if the stored object is attached to any workflow
$user = $token->getUser();
if ($user instanceof User && StoredObjectRoleEnum::SEE === $attributeAsEnum) {
foreach ($this->entityWorkflowAttachmentRepository->findByStoredObject($subject) as $workflowAttachment) {
if ($workflowAttachment->getEntityWorkflow()->isUserInvolved($user)) {
return true;
}
}
}
// Loop through context-specific voters
foreach ($this->storedObjectVoters as $storedObjectVoter) {
if ($storedObjectVoter->supports($attributeAsEnum, $subject)) {

View File

@@ -1,67 +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\DocStoreBundle\Serializer\Normalizer;
use Chill\DocStoreBundle\GenericDoc\Exception\AssociatedStoredObjectNotFound;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class GenericDocNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
/**
* Special key to attach a stored object to the generic doc.
*
* This is present for performance reason: if any other part of the application "knows" about the stored object
* related to the GenericDoc, this stored object is use instead of adding costly sql queries.
*/
public const ATTACHED_STORED_OBJECT_PROXY = 'attached-stored-object-proxy';
public function __construct(private readonly ManagerInterface $manager) {}
public function normalize($object, ?string $format = null, array $context = []): array
{
/* @var GenericDocDTO $object */
try {
$storedObject = $context[self::ATTACHED_STORED_OBJECT_PROXY] ?? $this->manager->fetchStoredObject($object);
} catch (AssociatedStoredObjectNotFound) {
$storedObject = null;
}
$data = [
'type' => 'doc_store_generic_doc',
'key' => $object->key,
'uniqueKey' => $object->key.implode('', array_keys($object->identifiers)).implode('', array_values($object->identifiers)),
'identifiers' => $object->identifiers,
'context' => $object->getContext(),
'doc_date' => $this->normalizer->normalize($object->docDate, $format, $context),
'metadata' => [],
'storedObject' => $this->normalizer->normalize($storedObject, $format, $context),
];
if ($this->manager->isGenericDocNormalizable($object, $format, $context)) {
$data['metadata'] = $this->manager->normalizeGenericDoc($object, $format, $context);
}
return $data;
}
public function supportsNormalization($data, ?string $format = null): bool
{
return 'json' === $format && $data instanceof GenericDocDTO;
}
}

View File

@@ -11,13 +11,10 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Tests\GenericDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\Exception\NotNormalizableGenericDocException;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\GenericDoc\Manager;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -61,7 +58,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
[],
$this->connection,
);
@@ -83,7 +79,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
[],
$this->connection,
);
@@ -105,7 +100,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
[],
$this->connection,
);
@@ -127,7 +121,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
[],
$this->connection,
);
@@ -149,7 +142,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
[],
$this->connection,
);
@@ -171,7 +163,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()],
[],
$this->connection,
);
@@ -179,77 +170,10 @@ class ManagerTest extends KernelTestCase
self::assertEquals(['accompanying_course_document_dummy'], $places);
}
public function testIsGenericDocNormalizable(): void
{
$genericDoc = new GenericDocDTO('test', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod());
$manager = new Manager([], [], [$this->buildNormalizer(true)], $this->connection);
self::assertTrue($manager->isGenericDocNormalizable($genericDoc, 'json'));
$manager = new Manager([], [], [$this->buildNormalizer(false)], $this->connection);
self::assertFalse($manager->isGenericDocNormalizable($genericDoc, 'json'));
}
public function testNormalizeGenericDocMetadata(): void
{
$genericDoc = new GenericDocDTO('test', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod());
$manager = new Manager([], [], [$this->buildNormalizer(false), $this->buildNormalizer(true)], $this->connection);
self::assertEquals(['title' => 'Some title'], $manager->normalizeGenericDoc($genericDoc, 'json'));
}
public function testNormalizeGenericDocMetadataNoNormalizer(): void
{
$genericDoc = new GenericDocDTO('test', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod());
$manager = new Manager([], [], [$this->buildNormalizer(false)], $this->connection);
$this->expectException(NotNormalizableGenericDocException::class);
self::assertEquals(['title' => 'Some title'], $manager->normalizeGenericDoc($genericDoc, 'json'));
}
public function buildNormalizer(bool $supports): GenericDocNormalizerInterface
{
return new class ($supports) implements GenericDocNormalizerInterface {
public function __construct(private readonly bool $supports) {}
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
return $this->supports;
}
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
return ['title' => 'Some title'];
}
};
}
}
final readonly class SimpleGenericDocAccompanyingPeriodProvider implements GenericDocForAccompanyingPeriodProviderInterface
{
public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
{
throw new \BadMethodCallException('not implemented');
}
public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
{
throw new \BadMethodCallException('not implemented');
}
public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
{
return 'accompanying_course_document_dummy' === $key;
}
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
{
return new GenericDocDTO('accompanying_course_document_dummy', $identifiers, new \DateTimeImmutable(), new AccompanyingPeriod());
}
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
$query = new FetchQuery(

View File

@@ -13,7 +13,6 @@ namespace Chill\DocStoreBundle\Tests\GenericDoc\Providers;
use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder;
use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericDocProvider;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface;
@@ -57,8 +56,7 @@ class AccompanyingCourseDocumentGenericDocProviderTest extends KernelTestCase
$provider = new AccompanyingCourseDocumentGenericDocProvider(
$security->reveal(),
$this->entityManager,
$this->prophesize(AccompanyingCourseDocumentRepository::class)->reveal()
$this->entityManager
);
$query = $provider->buildFetchQueryForAccompanyingPeriod($period, $startDate, $endDate, $content);

View File

@@ -14,7 +14,6 @@ namespace Chill\DocStoreBundle\Tests\GenericDoc\Providers;
use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\PhpUnit\ProphecyTrait;
@@ -34,14 +33,11 @@ class PersonDocumentGenericDocProviderTest extends KernelTestCase
private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository;
private PersonDocumentRepository $personDocumentRepository;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
$this->personDocumentACLAwareRepository = self::getContainer()->get(PersonDocumentACLAwareRepositoryInterface::class);
$this->personDocumentRepository = self::getContainer()->get(PersonDocumentRepository::class);
}
/**
@@ -64,8 +60,7 @@ class PersonDocumentGenericDocProviderTest extends KernelTestCase
$provider = new PersonDocumentGenericDocProvider(
$security->reveal(),
$this->personDocumentACLAwareRepository,
$this->personDocumentRepository,
$this->personDocumentACLAwareRepository
);
$query = $provider->buildFetchQueryForPerson($person, $startDate, $endDate, $content);

View File

@@ -16,7 +16,6 @@ use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@@ -45,7 +44,7 @@ class StoredObjectVoterTest extends TestCase
->with($this->logicalOr($this->identicalTo('ROLE_USER'), $this->identicalTo('ROLE_ADMIN')))
->willReturn($securityIsGrantedResult);
$voter = new StoredObjectVoter($security, $storedObjectVoters, new NullLogger(), $this->createMock(EntityWorkflowAttachmentRepository::class));
$voter = new StoredObjectVoter($security, $storedObjectVoters, new NullLogger());
self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
}

View File

@@ -1,75 +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\DocGeneratorBundle\Tests\Serializer\Normalizer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
use Chill\DocStoreBundle\Serializer\Normalizer\GenericDocNormalizer;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
*
* @coversNothing
*/
class GenericDocNormalizerTest extends TestCase
{
private $normalizer;
private ManagerInterface $manager;
protected function setUp(): void
{
$this->manager = $this->createMock(ManagerInterface::class);
$this->normalizer = new GenericDocNormalizer($this->manager);
$innerNormalizer = $this->createMock(NormalizerInterface::class);
$innerNormalizer->method('normalize')
->willReturnCallback(fn ($date) => $date instanceof \DateTimeImmutable ? $date->format(DATE_ATOM) : null);
$this->normalizer->setNormalizer($innerNormalizer);
}
public function testNormalize()
{
$docDate = new \DateTimeImmutable('2023-10-01T15:03:01.012345Z');
$object = new GenericDocDTO(
'some_key',
['id' => 'id1', 'other_id' => 'id2'],
$docDate,
new AccompanyingPeriod(),
);
$expected = [
'type' => 'doc_store_generic_doc',
'key' => 'some_key',
'identifiers' => ['id' => 'id1', 'other_id' => 'id2'],
'context' => 'accompanying-period',
'doc_date' => $docDate->format(DATE_ATOM),
'uniqueKey' => 'some_keyidother_idid1id2',
'metadata' => [],
'storedObject' => null,
];
$this->manager->expects($this->once())->method('isGenericDocNormalizable')
->with($object, 'json', [])
->willReturn(true);
$actual = $this->normalizer->normalize($object, 'json', []);
$this->assertEquals($expected, $actual);
}
}

View File

@@ -21,7 +21,6 @@ use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface;
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvideThirdPartiesAssociated;
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvidePersonsAssociated;
@@ -70,7 +69,7 @@ final readonly class AccompanyingCourseDocumentWorkflowHandler implements Entity
return $this->translator->trans('workflow.Document deleted');
}
return $this->translator->trans('entity_display_title.Document (n°%doc%)', ['%doc%' => $entityWorkflow->getRelatedEntityId()])
return $this->translator->trans('workflow.Document (n°%doc%)', ['%doc%' => $entityWorkflow->getRelatedEntityId()])
.' - '.$doc->getTitle();
}
@@ -79,15 +78,6 @@ final readonly class AccompanyingCourseDocumentWorkflowHandler implements Entity
return $this->repository->find($entityWorkflow->getRelatedEntityId());
}
public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod
{
if (null === $document = $this->getRelatedEntity($entityWorkflow)) {
return null;
}
return $document->getCourse();
}
/**
* @return array[]
*/

View File

@@ -39,7 +39,6 @@ class WorkflowWithPublicViewDocumentHelper
'storedObject' => $storedObject,
'send' => $send,
'metadata' => $metadata,
'attachments' => $entityWorkflow->getAttachments(),
]
);
}

View File

@@ -19,22 +19,6 @@ components:
type: string
type:
type: string
GenericDoc:
type: object
properties:
type:
type: string
enum:
- doc_store_generic_doc
key:
type: string
context:
type: string
enum:
- person
- accompanying-period
doc_date:
$ref: '#/components/schemas/Date'
paths:
/1.0/doc-store/stored-object/create:
@@ -167,32 +151,4 @@ paths:
application/json:
schema:
type: object
/1.0/doc-store/generic-doc/by-period/{id}/index:
get:
tags:
- storedobject
summary: A list of generic doc associated with the accompanying period
parameters:
- in: path
name: id
required: true
allowEmptyValue: false
description: The id of the accompanying period
schema:
type: integer
responses:
200:
description: "OK"
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/PaginatedResult'
- type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/GenericDoc'
type: object

View File

@@ -31,10 +31,6 @@ services:
arguments:
$providersForAccompanyingPeriod: !tagged_iterator chill_doc_store.generic_doc_accompanying_period_provider
$providersForPerson: !tagged_iterator chill_doc_store.generic_doc_person_provider
$genericDocNormalizers: !tagged_iterator chill_doc_store.generic_doc_metadata_normalizer
Chill\DocStoreBundle\GenericDoc\ManagerInterface:
alias: Chill\DocStoreBundle\GenericDoc\Manager
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension: ~
@@ -48,9 +44,6 @@ services:
Chill\DocStoreBundle\GenericDoc\Renderer\:
resource: '../GenericDoc/Renderer/'
Chill\DocStoreBundle\GenericDoc\Normalizer\:
resource: '../GenericDoc/Normalizer/'
Chill\DocStoreBundle\Validator\:
resource: '../Validator'

View File

@@ -1,45 +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\Migrations\DocStore;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20241212112733 extends AbstractMigration
{
public function getDescription(): string
{
return 'Move the title of PersonDocument and AccompanyingCourseDocument to stored object';
}
public function up(Schema $schema): void
{
$this->addSql('UPDATE chill_doc.stored_object SET title = ac_doc.title FROM chill_doc.accompanyingcourse_document ac_doc WHERE ac_doc.object_id = stored_object.id');
$this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document DROP scope_id');
$this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document DROP title');
$this->addSql('UPDATE chill_doc.stored_object SET title = p_doc.title FROM chill_doc.person_document p_doc WHERE p_doc.object_id = stored_object.id');
$this->addSql('ALTER TABLE chill_doc.person_document DROP title');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD scope_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD title TEXT DEFAULT \'\' NOT NULL');
$this->addSql('UPDATE chill_doc.accompanyingcourse_document SET title = so.title FROM chill_doc.stored_object so WHERE object_id = so.id');
$this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD CONSTRAINT fk_a45098f6682b5931 FOREIGN KEY (scope_id) REFERENCES scopes (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX idx_a45098f6682b5931 ON chill_doc.accompanyingcourse_document (scope_id)');
$this->addSql('ALTER TABLE chill_doc.person_document ADD title TEXT DEFAULT \'\' NOT NULL');
$this->addSql('UPDATE chill_doc.person_document SET title = so.title FROM chill_doc.stored_object so WHERE object_id = so.id');
}
}

View File

@@ -86,7 +86,6 @@ workflow:
shared_doc: Document partagé
title: Document partagé
main_document: Document principal
attachment: Pièce jointe
# ROLES
accompanyingCourseDocument: Documents dans les parcours d'accompagnement
@@ -95,7 +94,3 @@ CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE: Supprimer un document
CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE: Voir les documents
CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS: Voir les détails d'un document
CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE: Modifier un document
entity_display_title:
Document (n°%doc%): "Document (n°%doc%)"
Doc for evaluation (n°%eval%): Document de l'évaluation n°%eval%

View File

@@ -1,2 +0,0 @@
entity_display_title:
Doc for evaluation (n°%eval%): Evaluatiedocument (n°%eval%)

View File

@@ -18,6 +18,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\ExportsCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\ShortMessageCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
@@ -72,5 +73,6 @@ class ChillMainBundle extends Bundle
$container->addCompilerPass(new MenuCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ACLFlagsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new CRUDControllerCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ShortMessageCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
}
}

View File

@@ -309,7 +309,6 @@ class NotificationController extends AbstractController
$templateData[] = [
'template' => $this->notificationHandlerManager->getTemplate($notification),
'template_data' => $this->notificationHandlerManager->getTemplateData($notification),
'handler' => $this->notificationHandlerManager->getHandler($notification),
'notification' => $notification,
];
}

View File

@@ -95,7 +95,6 @@ class UserController extends CRUDController
return $this->render('@ChillMain/User/edit.html.twig', [
'entity' => $user,
'access_permissions_group_list' => $this->parameterBag->get('chill_main.access_permissions_group_list'),
'edit_form' => $this->createEditForm($user)->createView(),
'add_groupcenter_form' => $this->createAddLinkGroupCenterForm($user, $request)->createView(),
'delete_groupcenter_form' => array_map(
static fn (Form $form) => $form->createView(),

View File

@@ -1,101 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
use Chill\MainBundle\Workflow\Attachment\AddAttachmentAction;
use Chill\MainBundle\Workflow\Attachment\AddAttachmentRequestDTO;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class WorkflowAttachmentController
{
public function __construct(
private readonly Security $security,
private readonly SerializerInterface $serializer,
private readonly ValidatorInterface $validator,
private readonly EntityManagerInterface $entityManager,
private readonly AddAttachmentAction $addAttachmentAction,
) {}
#[Route('/api/1.0/main/workflow/{id}/attachment', methods: ['POST'])]
public function addAttachment(EntityWorkflow $entityWorkflow, Request $request): JsonResponse
{
if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $entityWorkflow)) {
throw new AccessDeniedHttpException();
}
$dto = new AddAttachmentRequestDTO($entityWorkflow);
$this->serializer->deserialize($request->getContent(), AddAttachmentRequestDTO::class, 'json', [
AbstractNormalizer::OBJECT_TO_POPULATE => $dto, AbstractNormalizer::GROUPS => ['write'],
]);
$errors = $this->validator->validate($dto);
if (count($errors) > 0) {
return new JsonResponse(
$this->serializer->serialize($errors, 'json'),
Response::HTTP_UNPROCESSABLE_ENTITY,
json: true
);
}
$attachment = ($this->addAttachmentAction)($dto);
$this->entityManager->flush();
return new JsonResponse(
$this->serializer->serialize($attachment, 'json', [AbstractNormalizer::GROUPS => ['read']]),
json: true
);
}
#[Route('/api/1.0/main/workflow/attachment/{id}', methods: ['DELETE'])]
public function removeAttachment(EntityWorkflowAttachment $attachment): Response
{
if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $attachment->getEntityWorkflow())) {
throw new AccessDeniedHttpException();
}
$this->entityManager->remove($attachment);
$this->entityManager->flush();
return new Response(null, Response::HTTP_NO_CONTENT);
}
#[Route('/api/1.0/main/workflow/{id}/attachment', methods: ['GET'])]
public function listAttachmentsForEntityWorkflow(EntityWorkflow $entityWorkflow): JsonResponse
{
if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $entityWorkflow)) {
throw new AccessDeniedHttpException();
}
return new JsonResponse(
$this->serializer->serialize(
$entityWorkflow->getAttachments(),
'json',
[AbstractNormalizer::GROUPS => ['read']]
),
json: true
);
}
}

View File

@@ -351,7 +351,6 @@ class WorkflowController extends AbstractController
'entity_workflow' => $entityWorkflow,
'transition_form_errors' => $errors,
'signatures' => $signatures,
'related_accompanying_period' => $this->entityWorkflowManager->getRelatedAccompanyingPeriod($entityWorkflow),
]
);
}

View File

@@ -234,8 +234,6 @@ class ChillMainExtension extends Extension implements
public function prepend(ContainerBuilder $container)
{
$this->prependNotifierTexterWithLegacyData($container);
// add installation_name and date_format to globals
$chillMainConfig = $container->getExtensionConfig($this->getAlias());
$config = $this->processConfiguration($this
@@ -359,55 +357,6 @@ class ChillMainExtension extends Extension implements
// Note: the controller are loaded inside compiler pass
}
/**
* This method prepend framework configuration with legacy configuration from "ovhCloudTransporter".
*
* It can be safely removed when the option chill_main.short_message.dsn will be removed.
*/
private function prependNotifierTexterWithLegacyData(ContainerBuilder $container): void
{
foreach (array_reverse($container->getExtensionConfig('framework')) as $c) {
// we look into each configuration for framework. If there is a configuration for
// texter_transports in one of them, we don't configure anything else
if (null !== $notifConfig = $c['notifier'] ?? null) {
if (null !== ($notifConfig['texter_transports'] ?? null)) {
return;
}
}
}
// there is no texter config, we try to configure one
$configs = $container->getExtensionConfig('chill_main');
$notifierSet = false;
foreach (array_reverse($configs) as $config) {
if (!array_key_exists('short_messages', $config)) {
continue;
}
if (array_key_exists('dsn', $config['short_messages'])) {
$container->prependExtensionConfig('framework', [
'notifier' => [
'texter_transports' => [
'ovh_legacy' => $config['short_messages']['dsn'],
],
],
]);
$notifierSet = true;
}
}
if (!$notifierSet) {
$container->prependExtensionConfig('framework', [
'notifier' => [
'texter_transports' => [
'dummy' => 'null://null',
],
],
]);
}
}
protected function prependCruds(ContainerBuilder $container)
{
$container->prependExtensionConfig('chill_main', [

View File

@@ -0,0 +1,91 @@
<?php
/**
* Chill is a software for social workers.
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\DependencyInjection\CompilerPass;
use Chill\MainBundle\Service\ShortMessage\NullShortMessageSender;
use Chill\MainBundle\Service\ShortMessage\ShortMessageTransporter;
use Chill\MainBundle\Service\ShortMessageOvh\OvhShortMessageSender;
use libphonenumber\PhoneNumberUtil;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
class ShortMessageCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$config = $container->resolveEnvPlaceholders($container->getParameter('chill_main.short_messages'), true);
// weird fix for special characters
$config['dsn'] = str_replace(['%%'], ['%'], (string) $config['dsn']);
$dsn = parse_url($config['dsn']);
parse_str($dsn['query'] ?? '', $dsn['queries']);
if ('null' === $dsn['scheme'] || false === $config['enabled']) {
$defaultTransporter = new Reference(NullShortMessageSender::class);
} elseif ('ovh' === $dsn['scheme']) {
if (!class_exists('\\'.\Ovh\Api::class)) {
throw new RuntimeException('Class \Ovh\Api not found');
}
foreach (['user', 'host', 'pass'] as $component) {
if (!\array_key_exists($component, $dsn)) {
throw new RuntimeException(sprintf('The component %s does not exist in dsn. Please provide a dsn like ovh://applicationKey:applicationSecret@endpoint?consumerKey=xxxx&sender=yyyy&service_name=zzzz', $component));
}
$container->setParameter('chill_main.short_messages.ovh_config_'.$component, $dsn[$component]);
}
foreach (['consumer_key', 'sender', 'service_name'] as $param) {
if (!\array_key_exists($param, $dsn['queries'])) {
throw new RuntimeException(sprintf('The parameter %s does not exist in dsn. Please provide a dsn like ovh://applicationKey:applicationSecret@endpoint?consumerKey=xxxx&sender=yyyy&service_name=zzzz', $param));
}
$container->setParameter('chill_main.short_messages.ovh_config_'.$param, $dsn['queries'][$param]);
}
$ovh = new Definition();
$ovh
->setClass('\\'.\Ovh\Api::class)
->setArgument(0, $dsn['user'])
->setArgument(1, $dsn['pass'])
->setArgument(2, $dsn['host'])
->setArgument(3, $dsn['queries']['consumer_key']);
$container->setDefinition(\Ovh\Api::class, $ovh);
$ovhSender = new Definition();
$ovhSender
->setClass(OvhShortMessageSender::class)
->setArgument(0, new Reference(\Ovh\Api::class))
->setArgument(1, $dsn['queries']['service_name'])
->setArgument(2, $dsn['queries']['sender'])
->setArgument(3, new Reference(LoggerInterface::class))
->setArgument(4, new Reference(PhoneNumberUtil::class));
$container->setDefinition(OvhShortMessageSender::class, $ovhSender);
$defaultTransporter = new Reference(OvhShortMessageSender::class);
} else {
throw new RuntimeException(sprintf('Cannot find a sender for this dsn: %s', $config['dsn']));
}
$container->getDefinition(ShortMessageTransporter::class)
->setArgument(0, $defaultTransporter);
}
}

View File

@@ -123,7 +123,6 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->arrayNode('short_messages')
->setDeprecated('chill-project/chill-bundles', '3.7.0', 'Since 3.7.0, Chill use the Notifier component to send message. Configure the notifier instead. In the meantime, the previous available OVH configuration will be append to the notifier component.')
->canBeEnabled()
->children()
->scalarNode('dsn')->cannotBeEmpty()->defaultValue('null://null')

View File

@@ -87,19 +87,12 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
private string $workflowName;
/**
* @var Collection<int, EntityWorkflowAttachment>
*/
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowAttachment::class, cascade: ['remove'], orphanRemoval: true)]
private Collection $attachments;
public function __construct()
{
$this->subscriberToFinal = new ArrayCollection();
$this->subscriberToStep = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->steps = new ArrayCollection();
$this->attachments = new ArrayCollection();
$initialStep = new EntityWorkflowStep();
$initialStep
@@ -149,35 +142,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
/**
* @return $this
*
* @internal use @{EntityWorkflowAttachement::__construct} instead
*/
public function addAttachment(EntityWorkflowAttachment $attachment): self
{
if (!$this->attachments->contains($attachment)) {
$this->attachments[] = $attachment;
}
return $this;
}
/**
* @return Collection<int, EntityWorkflowAttachment>
*/
public function getAttachments(): Collection
{
return $this->attachments;
}
public function removeAttachment(EntityWorkflowAttachment $attachment): self
{
$this->attachments->removeElement($attachment);
return $this;
}
public function getComments(): Collection
{
return $this->comments;
@@ -392,17 +356,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return $this->getCurrentStep()->isOnHoldByUser($user);
}
public function isUserInvolved(User $user): bool
{
foreach ($this->getSteps() as $step) {
if ($step->getAllDestUser()->contains($user)) {
return true;
}
}
return false;
}
public function isUserSubscribedToFinal(User $user): bool
{
return $this->subscriberToFinal->contains($user);
@@ -467,7 +420,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
}
/**
* Method used by marking store.
* Method use by marking store.
*
* @return $this
*/

View File

@@ -1,80 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Entity\Workflow;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity()]
#[ORM\Table(name: 'chill_main_workflow_entity_attachment')]
#[ORM\UniqueConstraint(name: 'unique_generic_doc_by_workflow', columns: ['relatedGenericDocKey', 'relatedGenericDocIdentifiers', 'entityworkflow_id'])]
class EntityWorkflowAttachment implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
public function __construct(
#[ORM\Column(name: 'relatedGenericDocKey', type: Types::STRING, length: 255, nullable: false)]
private string $relatedGenericDocKey,
#[ORM\Column(name: 'relatedGenericDocIdentifiers', type: Types::JSON, nullable: false, options: ['jsonb' => true])]
private array $relatedGenericDocIdentifiers,
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'attachments')]
#[ORM\JoinColumn(nullable: false, name: 'entityworkflow_id')]
private EntityWorkflow $entityWorkflow,
/**
* Stored object related to the generic doc.
*
* This is a story to keep track more easily to stored object
*/
#[ORM\ManyToOne(targetEntity: StoredObject::class)]
#[ORM\JoinColumn(nullable: false, name: 'storedobject_id')]
private StoredObject $proxyStoredObject,
) {
$this->entityWorkflow->addAttachment($this);
}
public function getId(): ?int
{
return $this->id;
}
public function getEntityWorkflow(): EntityWorkflow
{
return $this->entityWorkflow;
}
public function getRelatedGenericDocIdentifiers(): array
{
return $this->relatedGenericDocIdentifiers;
}
public function getRelatedGenericDocKey(): string
{
return $this->relatedGenericDocKey;
}
public function getProxyStoredObject(): StoredObject
{
return $this->proxyStoredObject;
}
}

View File

@@ -17,11 +17,6 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Doctrine\ORM\Mapping as ORM;
/**
* Contains comment for entity workflow.
*
* **NOTE**: for now, this class is not in used. Comments are, for now, stored in the EntityWorkflowStep.
*/
#[ORM\Entity]
#[ORM\Table('chill_main_workflow_entity_comment')]
class EntityWorkflowComment implements TrackCreationInterface, TrackUpdateInterface

View File

@@ -16,18 +16,9 @@ use Chill\MainBundle\Entity\UserGroup;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* A step for each EntityWorkflow.
*
* The step contains the history of position. The current one is the one which transitionAt or transitionAfter is NULL.
*
* The comments field is populated by the comment of the one who apply the transition, it means that the comment for the
* "next" step is stored in the EntityWorkflowStep in the previous step.
*
* DestUsers are the one added at the transition. DestUserByAccessKey are the users who obtained permission after having
* clicked on a link to get access (email notification to groups).
*/
#[ORM\Entity]
#[ORM\Table('chill_main_workflow_entity_step')]
class EntityWorkflowStep
@@ -89,11 +80,6 @@ class EntityWorkflowStep
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
/**
* If this is the final step.
*
* This property is filled by a listener.
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
private bool $isFinal = false;
@@ -268,11 +254,6 @@ class EntityWorkflowStep
return $this->ccUser;
}
/**
* This is the comment from the one who apply the transition.
*
* It means that it must be saved when the user apply a transition.
*/
public function getComment(): string
{
return $this->comment;
@@ -365,9 +346,6 @@ class EntityWorkflowStep
return $this->transitionByEmail;
}
/**
* @return bool true if this is the end of the EntityWorkflow
*/
public function isFinal(): bool
{
return $this->isFinal;
@@ -389,9 +367,6 @@ class EntityWorkflowStep
return false;
}
/**
* @return bool if the EntityWorkflowStep is waiting for a transition, and is not the final step
*/
public function isWaitingForTransition(): bool
{
if (null !== $this->transitionAfter) {
@@ -531,6 +506,26 @@ class EntityWorkflowStep
return $this->holdsOnStep;
}
#[Assert\Callback]
public function validateOnCreation(ExecutionContextInterface $context, mixed $payload): void
{
return;
if ($this->isFinalizeAfter()) {
if (0 !== \count($this->getDestUser())) {
$context->buildViolation('workflow.No dest users when the workflow is finalized')
->atPath('finalizeAfter')
->addViolation();
}
} else {
if (0 === \count($this->getDestUser())) {
$context->buildViolation('workflow.The next step must count at least one dest')
->atPath('finalizeAfter')
->addViolation();
}
}
}
public function addOnHold(EntityWorkflowStepHold $onHold): self
{
if (!$this->holdsOnStep->contains($onHold)) {

View File

@@ -53,7 +53,6 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate
public function __construct(
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
#[ORM\JoinColumn(nullable: false)]
private EntityWorkflowStep $step,
User|Person $signer,
) {

View File

@@ -29,7 +29,6 @@ class NewsItemType extends AbstractType
])
->add('content', ChillTextareaType::class, [
'required' => false,
'empty_data' => '',
])
->add(
'startDate',

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Notification;
use Chill\MainBundle\Entity\Notification;
use Symfony\Contracts\Translation\TranslatableInterface;
interface NotificationHandlerInterface
{
@@ -30,13 +29,4 @@ interface NotificationHandlerInterface
* Return true if the handler supports the handling for this notification.
*/
public function supports(Notification $notification, array $options = []): bool;
public function getTitle(Notification $notification, array $options = []): TranslatableInterface;
/*
* return list<Person>
*/
public function getAssociatedPersons(Notification $notification, array $options = []): array;
public function getRelatedEntity(Notification $notification): ?object;
}

View File

@@ -13,10 +13,11 @@ namespace Chill\MainBundle\Notification;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
use Doctrine\ORM\EntityManagerInterface;
final readonly class NotificationHandlerManager
{
public function __construct(private iterable $handlers) {}
public function __construct(private iterable $handlers, private EntityManagerInterface $em) {}
/**
* @throw NotificationHandlerNotFound if handler is not found

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