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}}' 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. # 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: >- 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") }} * {{ 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 "")) }}
**Schema Change**: {{ .Custom.SchemaChange }}
{{- end -}}
{{ if and (.Custom.Long) (not (eq .Custom.Long "")) }}{{ .Custom.Long }}{{ end }}
{{ .Custom.Long }}{{ end }}
custom: 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 - key: Issue
label: Issue number (on chill-bundles repository) (optional) label: Issue number (on chill-bundles repository) (optional)
optional: true optional: true
type: int type: int
minInt: 1 minInt: 1
body: body:
# allow multiline messages # allow multiline messages
block: true block: true

4
.env
View File

@@ -88,7 +88,3 @@ REDIS_HOST=redis
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT} REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
###< chill-project/chill-bundles ### ###< 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/ docs/build/
.php_cs.cache .php_cs.cache
.cache/* .cache/*
yarn.lock
docker/db/data docker/db/data
docker/rabbitmq/data docker/rabbitmq/data

View File

@@ -25,7 +25,7 @@ $config = new PhpCsFixer\Config();
$config $config
->setFinder($finder) ->setFinder($finder)
->setRiskyAllowed(true) ->setRiskyAllowed(true)
->setCacheFile('var/php-cs-fixer.cache') ->setCacheFile('.cache/php-cs-fixer.cache')
->setUsingCache(true) ->setUsingCache(true)
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) ->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). 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 ## v3.6.0 - 2025-01-16
### Feature ### 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. * 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": "*", "ext-zlib": "*",
"champs-libres/wopi-bundle": "dev-master@dev", "champs-libres/wopi-bundle": "dev-master@dev",
"champs-libres/wopi-lib": "dev-master@dev", "champs-libres/wopi-lib": "dev-master@dev",
"doctrine/data-fixtures": "^1.8",
"doctrine/doctrine-bundle": "^2.1", "doctrine/doctrine-bundle": "^2.1",
"doctrine/data-fixtures": "^1.8",
"doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.13.0", "doctrine/orm": "^2.13.0",
"erusev/parsedown": "^1.7", "erusev/parsedown": "^1.7",
@@ -58,9 +58,7 @@
"symfony/messenger": "^5.4", "symfony/messenger": "^5.4",
"symfony/mime": "^5.4", "symfony/mime": "^5.4",
"symfony/monolog-bundle": "^3.5", "symfony/monolog-bundle": "^3.5",
"symfony/notifier": "^5.4",
"symfony/options-resolver": "^5.4", "symfony/options-resolver": "^5.4",
"symfony/ovh-cloud-notifier": "^5.4",
"symfony/process": "^5.4", "symfony/process": "^5.4",
"symfony/property-access": "^5.4", "symfony/property-access": "^5.4",
"symfony/property-info": "^5.4", "symfony/property-info": "^5.4",
@@ -162,9 +160,7 @@
"cache:clear": "symfony-cmd", "cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd" "assets:install %PUBLIC_DIR%": "symfony-cmd"
}, },
"php-cs-fixer": "php-cs-fixer fix --config=./.php-cs-fixer.dist.php --show-progress=none", "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"
}, },
"extra": { "extra": {
"symfony": { "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``. 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. 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: Interesting options that can be used in combination with eslint are:
- ``--quiet`` to only get errors and silence the warnings - ``--quiet`` to only get errors and silence the warnings

View File

@@ -16,7 +16,7 @@ Welcome to Chill documentation!
Chill is a free software for social workers. Chill is a free software for social workers.
Chill rely on the php framework `Symfony <http://symfony.com>`_. Chill rely on the php framework `Symfony <http://symfony.com>`_.
Contents of this documentation: Contents of this documentation:
@@ -42,7 +42,7 @@ Contribute
User manual User manual
=========== ===========
An user manual exists in French and currently focuses on describing the main concept of the software. An user manual exists in French and currently focuses on describing the main concept of the software.
`Read (and contribute) to the manual <https://fr.wikibooks.org/wiki/Chill>`_ `Read (and contribute) to the manual <https://fr.wikibooks.org/wiki/Chill>`_
@@ -55,11 +55,12 @@ Available bundles
* Chill Person, to deal with persons, * Chill Person, to deal with persons,
* chill custom fields, to add custom fields to some entities, * chill custom fields, to add custom fields to some entities,
* chill activity: to add activities to people, * chill activity: to add activities to people,
* chill report: to add report to people, * chill report: to add report to people,
* chill event: to gather people into events, * chill event: to gather people into events,
* chill docs store: to store documents to people, but also entities, * chill docs store: to store documents to people, but also entities,
* chill task: to register task with people, * chill task: to register task with people,
* chill third party: to register third parties, * chill third party: to register third parties,
* chill family members: to register family members
You will also found the following projects : You will also found the following projects :

View File

@@ -16,7 +16,7 @@
"@eslint/js": "^9.14.0", "@eslint/js": "^9.14.0",
"@luminateone/eslint-baseline": "^1.0.9", "@luminateone/eslint-baseline": "^1.0.9",
"@symfony/webpack-encore": "^4.1.0", "@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node20": "^20.1.4", "@tsconfig/node14": "^1.0.1",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"@typescript-eslint/parser": "^8.12.2", "@typescript-eslint/parser": "^8.12.2",
@@ -30,6 +30,7 @@
"eslint-plugin-vue": "^9.30.0", "eslint-plugin-vue": "^9.30.0",
"fork-awesome": "^1.1.7", "fork-awesome": "^1.1.7",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"marked": "^12.0.1",
"node-sass": "^8.0.0", "node-sass": "^8.0.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"postcss-loader": "^7.0.2", "postcss-loader": "^7.0.2",
@@ -79,12 +80,7 @@
"dev": "encore dev", "dev": "encore dev",
"watch": "encore dev --watch", "watch": "encore dev --watch",
"build": "encore production --progress", "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", "eslint": "npx eslint-baseline \"**/*.{js,ts,vue}\""
"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}\""
}, },
"private": true "private": true
} }

View File

@@ -20,10 +20,6 @@ return static function (RectorConfig $rectorConfig): void {
__DIR__ . '/src', __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->symfonyContainerXml(__DIR__ . '/var/cache/dev/test/App_KernelTestDebugContainer.xml ');
$rectorConfig->symfonyContainerPhp(__DIR__ . '/tests/symfony-container.php'); $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\ActivityBundle\Repository\ActivityRepository;
use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\NotificationHandlerInterface; 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 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 public function getTemplate(Notification $notification, array $options = []): string
{ {
@@ -40,30 +37,4 @@ final readonly class ActivityNotificationHandler implements NotificationHandlerI
{ {
return Activity::class === $notification->getRelatedEntityClass(); 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 { createStore } from "vuex";
import { postLocation } from "./api"; import { postLocation } from "./api";
import prepareLocations from "./store.locations.js"; import prepareLocations from "./store.locations.js";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
const debug = process.env.NODE_ENV !== "production"; const debug = process.env.NODE_ENV !== "production";
//console.log('window.activity', window.activity); //console.log('window.activity', window.activity);
@@ -24,7 +23,6 @@ const removeIdFromValue = (string, id) => {
const store = createStore({ const store = createStore({
strict: debug, strict: debug,
state: { state: {
me: null,
activity: window.activity, activity: window.activity,
socialIssuesOther: [], socialIssuesOther: [],
socialActionsList: [], socialActionsList: [],
@@ -81,25 +79,15 @@ const store = createStore({
); );
}, },
suggestedUser(state) { suggestedUser(state) {
// console.log('current user', state.me)
const existingUserIds = state.activity.users.map((p) => p.id); const existingUserIds = state.activity.users.map((p) => p.id);
let suggestedUsers = return state.activity.activityType.usersVisible === 0
state.activity.activityType.usersVisible === 0 ? []
? [] : [state.activity.accompanyingPeriod.user].filter(
: [state.activity.accompanyingPeriod.user].filter( (u) => u !== null && !existingUserIds.includes(u.id),
(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) { suggestedResources(state) {
// const resources = state.activity.accompanyingPeriod.resources; const resources = state.activity.accompanyingPeriod.resources;
const existingPersonIds = state.activity.persons.map((p) => p.id); const existingPersonIds = state.activity.persons.map((p) => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map( const existingThirdPartyIds = state.activity.thirdParties.map(
(p) => p.id, (p) => p.id,
@@ -123,9 +111,6 @@ const store = createStore({
}, },
}, },
mutations: { mutations: {
setWhoAmI(state, me) {
state.me = me;
},
// SocialIssueAcc // SocialIssueAcc
addIssueInList(state, issue) { addIssueInList(state, issue) {
//console.log('add issue list', issue.id); //console.log('add issue list', issue.id);
@@ -341,17 +326,9 @@ const store = createStore({
} }
commit("updateLocation", value); 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); prepareLocations(store);
export default 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 %}"> <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> </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\Entity\Activity;
use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepositoryInterface; use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepositoryInterface;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -36,47 +34,8 @@ final readonly class AccompanyingPeriodActivityGenericDocProvider implements Gen
private EntityManagerInterface $em, private EntityManagerInterface $em,
private Security $security, private Security $security,
private ActivityDocumentACLAwareRepositoryInterface $activityDocumentACLAwareRepository, 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 public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{ {
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); $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\GenericDoc\Twig\GenericDocRendererInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepository; use Chill\DocStoreBundle\Repository\StoredObjectRepository;
/**
* @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}>
*/
final readonly class AccompanyingPeriodActivityGenericDocRenderer implements GenericDocRendererInterface final readonly class AccompanyingPeriodActivityGenericDocRenderer implements GenericDocRendererInterface
{ {
public function __construct(private StoredObjectRepository $objectRepository, private ActivityRepository $activityRepository) {} 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 public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
{ {
return ($options['row-only'] ?? false) ? '@ChillActivity/GenericDoc/activity_document_row.html.twig' : return '@ChillActivity/GenericDoc/activity_document.html.twig';
'@ChillActivity/GenericDoc/activity_document.html.twig';
} }
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array 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']), 'activity' => $this->activityRepository->find($genericDocDTO->identifiers['activity_id']),
'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']), 'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']),
'context' => $genericDocDTO->getContext(), 'context' => $genericDocDTO->getContext(),
'show_actions' => $options['show-actions'] ?? true,
]; ];
} }
} }

View File

@@ -14,5 +14,3 @@ export:
describe_action_with_subject: >- 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} 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 Insert a document: Insérer un document
Remove a document: Supprimer le document Remove a document: Supprimer le document
comment: Commentaire comment: Commentaire
deleted: Échange supprimé
No documents: Aucun document No documents: Aucun document
# activity filter in list page # activity filter in list page

View File

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

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Repository; namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\CalendarDoc; use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
@@ -50,21 +49,4 @@ class CalendarDocRepository implements ObjectRepository, CalendarDocRepositoryIn
{ {
return CalendarDoc::class; 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; namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\CalendarDoc; use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
interface CalendarDocRepositoryInterface interface CalendarDocRepositoryInterface
{ {
@@ -30,7 +29,5 @@ interface CalendarDocRepositoryInterface
public function findOneBy(array $criteria): ?CalendarDoc; public function findOneBy(array $criteria): ?CalendarDoc;
public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc;
public function getClassName(); public function getClassName();
} }

View File

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

View File

@@ -5,5 +5,71 @@
{% set c = document.calendar %} {% set c = document.calendar %}
<div class="item-bloc"> <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> </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\Calendar;
use Chill\CalendarBundle\Entity\CalendarDoc; use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\CalendarBundle\Repository\CalendarDocRepositoryInterface;
use Chill\CalendarBundle\Security\Voter\CalendarVoter; use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -40,38 +38,8 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen
public function __construct( public function __construct(
private Security $security, private Security $security,
private EntityManagerInterface $em, 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 * @throws MappingException
*/ */
@@ -114,7 +82,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen
[Types::INTEGER] [Types::INTEGER]
); );
return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content); return $query;
} }
public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool 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\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
/**
* @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}>
*/
final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements GenericDocRendererInterface final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements GenericDocRendererInterface
{ {
public function __construct(private CalendarDocRepository $repository) {} public function __construct(private CalendarDocRepository $repository) {}
@@ -31,8 +28,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements Gen
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
{ {
return $options['row-only'] ?? false ? '@ChillCalendar/GenericDoc/calendar_document_row.html.twig' return '@ChillCalendar/GenericDoc/calendar_document.html.twig';
: '@ChillCalendar/GenericDoc/calendar_document.html.twig';
} }
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
@@ -40,7 +36,6 @@ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements Gen
return [ return [
'document' => $this->repository->find($genericDocDTO->identifiers['id']), 'document' => $this->repository->find($genericDocDTO->identifiers['id']),
'context' => $genericDocDTO->getContext(), '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 Chill\CalendarBundle\Entity\Calendar;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Notifier\TexterInterface; use Symfony\Component\Messenger\MessageBusInterface;
class BulkCalendarShortMessageSender class BulkCalendarShortMessageSender
{ {
public function __construct( public function __construct(private readonly CalendarForShortMessageProvider $provider, private readonly EntityManagerInterface $em, private readonly LoggerInterface $logger, private readonly MessageBusInterface $messageBus, private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder) {}
private readonly CalendarForShortMessageProvider $provider,
private readonly EntityManagerInterface $em,
private readonly LoggerInterface $logger,
private readonly TexterInterface $texter,
private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder,
) {}
public function sendBulkMessageToEligibleCalendars() public function sendBulkMessageToEligibleCalendars()
{ {
@@ -42,7 +36,7 @@ class BulkCalendarShortMessageSender
$smses = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar); $smses = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar);
foreach ($smses as $sms) { foreach ($smses as $sms) {
$this->texter->send($sms); $this->messageBus->dispatch($sms);
++$countSms; ++$countSms;
} }

View File

@@ -19,26 +19,12 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Service\ShortMessageNotification; namespace Chill\CalendarBundle\Service\ShortMessageNotification;
use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Entity\Calendar;
use libphonenumber\PhoneNumberFormat; use Chill\MainBundle\Service\ShortMessage\ShortMessage;
use libphonenumber\PhoneNumberUtil;
use Symfony\Component\Notifier\Message\SmsMessage;
class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBuilderInterface 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 public function buildMessageForCalendar(Calendar $calendar): array
{ {
if (true !== $calendar->getSendSMS()) { if (true !== $calendar->getSendSMS()) {
@@ -53,14 +39,16 @@ class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBu
} }
if (Calendar::SMS_PENDING === $calendar->getSmsStatus()) { if (Calendar::SMS_PENDING === $calendar->getSmsStatus()) {
$toUsers[] = new SmsMessage( $toUsers[] = new ShortMessage(
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]), $this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]),
$person->getMobilenumber(),
ShortMessage::PRIORITY_LOW
); );
} elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) { } elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) {
$toUsers[] = new SmsMessage( $toUsers[] = new ShortMessage(
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]), $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; namespace Chill\CalendarBundle\Service\ShortMessageNotification;
use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Entity\Calendar;
use Symfony\Component\Notifier\Message\SmsMessage; use Chill\MainBundle\Service\ShortMessage\ShortMessage;
interface ShortMessageForCalendarBuilderInterface interface ShortMessageForCalendarBuilderInterface
{ {
/** /**
* @return list<SmsMessage> * @return array|ShortMessage[]
*/ */
public function buildMessageForCalendar(Calendar $calendar): array; 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\CalendarForShortMessageProvider;
use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface; use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Service\ShortMessage\ShortMessage;
use Chill\MainBundle\Test\PrepareUserTrait; use Chill\MainBundle\Test\PrepareUserTrait;
use Chill\PersonBundle\DataFixtures\Helper\PersonRandomHelper; use Chill\PersonBundle\DataFixtures\Helper\PersonRandomHelper;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use libphonenumber\PhoneNumberUtil;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Notifier\Message\SentMessage; use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Notifier\Message\SmsMessage; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Notifier\TexterInterface;
/** /**
* @internal * @internal
@@ -100,23 +101,24 @@ final class BulkCalendarShortMessageSenderTest extends KernelTestCase
$messageBuilder->buildMessageForCalendar(Argument::type(Calendar::class)) $messageBuilder->buildMessageForCalendar(Argument::type(Calendar::class))
->willReturn( ->willReturn(
[ [
new SmsMessage( new ShortMessage(
'+32470123456',
'content', 'content',
PhoneNumberUtil::getInstance()->parse('+32470123456', 'BE'),
ShortMessage::PRIORITY_MEDIUM
), ),
] ]
); );
$texter = $this->prophesize(TexterInterface::class); $bus = $this->prophesize(MessageBusInterface::class);
$texter->send(Argument::type(SmsMessage::class)) $bus->dispatch(Argument::type(ShortMessage::class))
->will(fn ($args): SentMessage => new SentMessage($args[0], 'sms')) ->willReturn(new Envelope(new \stdClass()))
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$bulk = new BulkCalendarShortMessageSender( $bulk = new BulkCalendarShortMessageSender(
$provider->reveal(), $provider->reveal(),
$em, $em,
new NullLogger(), new NullLogger(),
$texter->reveal(), $bus->reveal(),
$messageBuilder->reveal() $messageBuilder->reveal()
); );

View File

@@ -23,6 +23,7 @@ use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultShortMessageFor
use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil; use libphonenumber\PhoneNumberUtil;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
@@ -89,9 +90,10 @@ final class DefaultShortMessageForCalendarBuilderTest extends TestCase
$this->assertCount(1, $sms); $this->assertCount(1, $sms);
$this->assertEquals( $this->assertEquals(
'+32470123456', '+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 // if the calendar is canceled
$calendar $calendar
@@ -103,8 +105,9 @@ final class DefaultShortMessageForCalendarBuilderTest extends TestCase
$this->assertCount(1, $sms); $this->assertCount(1, $sms);
$this->assertEquals( $this->assertEquals(
'+32470123456', '+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\DependencyInjection\Compiler\StorageConfigurationCompilerPass;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -29,8 +28,6 @@ class ChillDocStoreBundle extends Bundle
->addTag('chill_doc_store.generic_doc_person_provider'); ->addTag('chill_doc_store.generic_doc_person_provider');
$container->registerForAutoconfiguration(GenericDocRendererInterface::class) $container->registerForAutoconfiguration(GenericDocRendererInterface::class)
->addTag('chill_doc_store.generic_doc_renderer'); ->addTag('chill_doc_store.generic_doc_renderer');
$container->registerForAutoconfiguration(GenericDocNormalizerInterface::class)
->addTag('chill_doc_store.generic_doc_metadata_normalizer');
$container->addCompilerPass(new StorageConfigurationCompilerPass()); $container->addCompilerPass(new StorageConfigurationCompilerPass());
} }

View File

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

View File

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

View File

@@ -46,10 +46,9 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
#[ORM\ManyToOne(targetEntity: DocGeneratorTemplate::class)] #[ORM\ManyToOne(targetEntity: DocGeneratorTemplate::class)]
private ?DocGeneratorTemplate $template = null; private ?DocGeneratorTemplate $template = null;
/** #[Assert\Length(min: 2, max: 250)]
* Store the title of the document, if the title is set before the document. #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
*/ private string $title = '';
private string $proxyTitle = '';
#[ORM\ManyToOne(targetEntity: \Chill\MainBundle\Entity\User::class)] #[ORM\ManyToOne(targetEntity: \Chill\MainBundle\Entity\User::class)]
private ?\Chill\MainBundle\Entity\User $user = null; private ?\Chill\MainBundle\Entity\User $user = null;
@@ -79,10 +78,9 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
return $this->template; return $this->template;
} }
#[Assert\Length(min: 2, max: 250)]
public function getTitle(): string public function getTitle(): string
{ {
return (string) $this->getObject()?->getTitle(); return $this->title;
} }
public function getUser() public function getUser()
@@ -115,10 +113,6 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
{ {
$this->object = $object; $this->object = $object;
if ('' !== $this->proxyTitle) {
$this->object->setTitle($this->proxyTitle);
}
return $this; return $this;
} }
@@ -131,11 +125,7 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
public function setTitle(string $title): self public function setTitle(string $title): self
{ {
if (null !== $this->getObject()) { $this->title = $title;
$this->getObject()->setTitle($title);
} else {
$this->proxyTitle = $title;
}
return $this; 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; use Chill\PersonBundle\Entity\AccompanyingPeriod;
interface GenericDocForAccompanyingPeriodProviderInterface extends GenericDocProviderInterface interface GenericDocForAccompanyingPeriodProviderInterface
{ {
public function buildFetchQueryForAccompanyingPeriod( public function buildFetchQueryForAccompanyingPeriod(
AccompanyingPeriod $accompanyingPeriod, 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; 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\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Doctrine\DBAL\Connection; use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
final readonly class Manager implements ManagerInterface final readonly class Manager
{ {
private FetchQueryToSqlBuilder $builder; private FetchQueryToSqlBuilder $builder;
@@ -34,16 +31,16 @@ final readonly class Manager implements ManagerInterface
* @var iterable<GenericDocForPersonProviderInterface> * @var iterable<GenericDocForPersonProviderInterface>
*/ */
private iterable $providersForPerson, private iterable $providersForPerson,
/**
* @var iterable<GenericDocNormalizerInterface>
*/
private iterable $genericDocNormalizers,
private Connection $connection, private Connection $connection,
) { ) {
$this->builder = new FetchQueryToSqlBuilder(); $this->builder = new FetchQueryToSqlBuilder();
} }
/**
* @param list<string> $places
*
* @throws Exception
*/
public function countDocForAccompanyingPeriod( public function countDocForAccompanyingPeriod(
AccompanyingPeriod $accompanyingPeriod, AccompanyingPeriod $accompanyingPeriod,
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,
@@ -86,6 +83,13 @@ final readonly class Manager implements ManagerInterface
return $this->countDoc($sql, $params, $types); 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( public function findDocForAccompanyingPeriod(
AccompanyingPeriod $accompanyingPeriod, AccompanyingPeriod $accompanyingPeriod,
int $offset = 0, 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( public function findDocForPerson(
Person $person, Person $person,
int $offset = 0, int $offset = 0,
@@ -182,28 +161,6 @@ final readonly class Manager implements ManagerInterface
return $this->places($sql, $params, $types); 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 private function places(string $sql, array $params, array $types): array
{ {
if ('' === $sql) { 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; namespace Chill\DocStoreBundle\GenericDoc\Providers;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
@@ -34,47 +31,17 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
public function __construct( public function __construct(
private Security $security, private Security $security,
private EntityManagerInterface $entityManager, 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 public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{ {
$classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class); $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
$query = new FetchQuery( $query = new FetchQuery(
self::KEY, 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->getColumnName('date'),
$classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS acc_course_document' $classMetadata->getSchemaName().'.'.$classMetadata->getTableName()
); );
$query->addWhereClause( $query->addWhereClause(
@@ -97,7 +64,7 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
$query = new FetchQuery( $query = new FetchQuery(
self::KEY, 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->getColumnName('date'),
$classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS acc_course_document' $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 private function addWhereClause(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{ {
$classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class); $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
$storedObjectMetadata = $this->entityManager->getClassMetadata(StoredObject::class);
if (null !== $startDate) { if (null !== $startDate) {
$query->addWhereClause( $query->addWhereClause(
@@ -162,19 +128,9 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
} }
if (null !== $content and '' !== $content) { 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( $query->addWhereClause(
sprintf( sprintf(
'(doc_store.%s ilike ? OR acc_course_document.%s ilike ?)', '(%s ilike ? OR %s ilike ?)',
$classMetadata->getColumnName('title'), $classMetadata->getColumnName('title'),
$classMetadata->getColumnName('description') $classMetadata->getColumnName('description')
), ),

View File

@@ -11,13 +11,10 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\GenericDoc\Providers; namespace Chill\DocStoreBundle\GenericDoc\Providers;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface; use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
@@ -30,38 +27,8 @@ final readonly class PersonDocumentGenericDocProvider implements GenericDocForPe
public function __construct( public function __construct(
private Security $security, private Security $security,
private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository, 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( public function buildFetchQueryForPerson(
Person $person, Person $person,
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,

View File

@@ -18,9 +18,6 @@ use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericD
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository; use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
/**
* @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}>
*/
final readonly class AccompanyingCourseDocumentGenericDocRenderer implements GenericDocRendererInterface final readonly class AccompanyingCourseDocumentGenericDocRenderer implements GenericDocRendererInterface
{ {
public function __construct( public function __construct(
@@ -36,10 +33,6 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string 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'; return '@ChillDocStore/List/list_item.html.twig';
} }
@@ -51,7 +44,6 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen
'accompanyingCourse' => $doc->getCourse(), 'accompanyingCourse' => $doc->getCourse(),
'options' => $options, 'options' => $options,
'context' => $genericDocDTO->getContext(), 'context' => $genericDocDTO->getContext(),
'show_actions' => $options['show-actions'] ?? true,
]; ];
} }
@@ -61,7 +53,6 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen
'person' => $doc->getPerson(), 'person' => $doc->getPerson(),
'options' => $options, 'options' => $options,
'context' => $genericDocDTO->getContext(), '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; use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
/**
* Render a generic doc, to display it into a page.
*
* @template T of array
*/
interface GenericDocRendererInterface interface GenericDocRendererInterface
{ {
/**
* @param T $options the options defined by the renderer
*/
public function supports(GenericDocDTO $genericDocDTO, $options = []): bool; public function supports(GenericDocDTO $genericDocDTO, $options = []): bool;
/**
* @param T $options the options defined by the renderer
*/
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string; public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string;
/**
* @param T $options the options defined by the renderer
*/
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array; public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array;
} }

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Repository; namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\PersonDocument; use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider; 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 private function addFilterClauses(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{ {
$personDocMetadata = $this->em->getClassMetadata(PersonDocument::class); $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class);
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
if (null !== $startDate) { if (null !== $startDate) {
$query->addWhereClause( $query->addWhereClause(
@@ -156,20 +154,10 @@ final readonly class PersonDocumentACLAwareRepository implements PersonDocumentA
} }
if (null !== $content and '' !== $content) { 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( $query->addWhereClause(
sprintf( sprintf(
'(doc_store.%s ilike ? OR person_document.%s ilike ?)', '(%s ilike ? OR %s ilike ?)',
$storedObjectMetadata->getColumnName('title'), $personDocMetadata->getColumnName('title'),
$personDocMetadata->getColumnName('description') $personDocMetadata->getColumnName('description')
), ),
['%'.$content.'%', '%'.$content.'%'], ['%'.$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 DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
import { createApp } from "vue"; import { createApp } from "vue";
import { StoredObject, StoredObjectStatusChange } from "../../types"; import { StoredObject, StoredObjectStatusChange } from "../../types";

View File

@@ -1,5 +1,8 @@
import { DateTime, User } from "ChillMainAssets/types"; import {
import { SignedUrlGet } from "ChillDocStoreAssets/vuejs/StoredObjectButton/helpers"; DateTime,
User,
} from "../../../ChillMainBundle/Resources/public/types";
import { SignedUrlGet } from "./vuejs/StoredObjectButton/helpers";
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending"; export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
@@ -135,10 +138,3 @@ export interface ZoomLevel {
nl?: string; 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 { function buildDocumentName(): string {
let document_name = props.filename ?? props.storedObject.title; let document_name = props.filename ?? props.storedObject.title;
if ("" === document_name || null === document_name) { if ("" === document_name) {
document_name = "document"; 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"> <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> </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 %}
{% endif %} {% endif %}
<div class="row g-3"> <div class="row">
<div class="col-xs-12 col-sm-6 col-md-4"> <div class="col-xs-12 col-sm-6 col-md-4">
<div class="card"> <div class="card"">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">{{ title }}</h2> <h2 class="card-title">{{ title }}</h2>
<h3>{{ 'workflow.public_link.main_document'|trans }}</h3> <h3>{{ 'workflow.public_link.main_document'|trans }}</h3>
@@ -39,21 +39,5 @@
</div> </div>
</div> </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> </div>
{% endblock %} {% endblock %}

View File

@@ -12,8 +12,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Security\Authorization; namespace Chill\DocStoreBundle\Security\Authorization;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
@@ -28,12 +26,7 @@ class StoredObjectVoter extends Voter
{ {
public const LOG_PREFIX = '[stored object voter] '; public const LOG_PREFIX = '[stored object voter] ';
public function __construct( public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters, private readonly LoggerInterface $logger) {}
private readonly Security $security,
private readonly iterable $storedObjectVoters,
private readonly LoggerInterface $logger,
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
) {}
protected function supports($attribute, $subject): bool protected function supports($attribute, $subject): bool
{ {
@@ -46,16 +39,6 @@ class StoredObjectVoter extends Voter
/** @var StoredObject $subject */ /** @var StoredObject $subject */
$attributeAsEnum = StoredObjectRoleEnum::from($attribute); $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 // Loop through context-specific voters
foreach ($this->storedObjectVoters as $storedObjectVoter) { foreach ($this->storedObjectVoters as $storedObjectVoter) {
if ($storedObjectVoter->supports($attributeAsEnum, $subject)) { 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; 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\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\GenericDoc\Manager; use Chill\DocStoreBundle\GenericDoc\Manager;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -61,7 +58,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager( $manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()], [new SimpleGenericDocPersonProvider()],
[],
$this->connection, $this->connection,
); );
@@ -83,7 +79,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager( $manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()], [new SimpleGenericDocPersonProvider()],
[],
$this->connection, $this->connection,
); );
@@ -105,7 +100,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager( $manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()], [new SimpleGenericDocPersonProvider()],
[],
$this->connection, $this->connection,
); );
@@ -127,7 +121,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager( $manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()], [new SimpleGenericDocPersonProvider()],
[],
$this->connection, $this->connection,
); );
@@ -149,7 +142,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager( $manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()], [new SimpleGenericDocPersonProvider()],
[],
$this->connection, $this->connection,
); );
@@ -171,7 +163,6 @@ class ManagerTest extends KernelTestCase
$manager = new Manager( $manager = new Manager(
[new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocAccompanyingPeriodProvider()],
[new SimpleGenericDocPersonProvider()], [new SimpleGenericDocPersonProvider()],
[],
$this->connection, $this->connection,
); );
@@ -179,77 +170,10 @@ class ManagerTest extends KernelTestCase
self::assertEquals(['accompanying_course_document_dummy'], $places); 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 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 public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{ {
$query = new FetchQuery( $query = new FetchQuery(

View File

@@ -13,7 +13,6 @@ namespace Chill\DocStoreBundle\Tests\GenericDoc\Providers;
use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder; use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder;
use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericDocProvider; use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericDocProvider;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@@ -57,8 +56,7 @@ class AccompanyingCourseDocumentGenericDocProviderTest extends KernelTestCase
$provider = new AccompanyingCourseDocumentGenericDocProvider( $provider = new AccompanyingCourseDocumentGenericDocProvider(
$security->reveal(), $security->reveal(),
$this->entityManager, $this->entityManager
$this->prophesize(AccompanyingCourseDocumentRepository::class)->reveal()
); );
$query = $provider->buildFetchQueryForAccompanyingPeriod($period, $startDate, $endDate, $content); $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\FetchQueryToSqlBuilder;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider; use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface; use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\PhpUnit\ProphecyTrait;
@@ -34,14 +33,11 @@ class PersonDocumentGenericDocProviderTest extends KernelTestCase
private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository; private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository;
private PersonDocumentRepository $personDocumentRepository;
protected function setUp(): void protected function setUp(): void
{ {
self::bootKernel(); self::bootKernel();
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class); $this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
$this->personDocumentACLAwareRepository = self::getContainer()->get(PersonDocumentACLAwareRepositoryInterface::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( $provider = new PersonDocumentGenericDocProvider(
$security->reveal(), $security->reveal(),
$this->personDocumentACLAwareRepository, $this->personDocumentACLAwareRepository
$this->personDocumentRepository,
); );
$query = $provider->buildFetchQueryForPerson($person, $startDate, $endDate, $content); $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\StoredObjectVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 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'))) ->with($this->logicalOr($this->identicalTo('ROLE_USER'), $this->identicalTo('ROLE_ADMIN')))
->willReturn($securityIsGrantedResult); ->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])); 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\EntityWorkflowWithPublicViewInterface;
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface; use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO; use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvideThirdPartiesAssociated; use Chill\PersonBundle\Service\AccompanyingPeriod\ProvideThirdPartiesAssociated;
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvidePersonsAssociated; 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('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(); .' - '.$doc->getTitle();
} }
@@ -79,15 +78,6 @@ final readonly class AccompanyingCourseDocumentWorkflowHandler implements Entity
return $this->repository->find($entityWorkflow->getRelatedEntityId()); 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[] * @return array[]
*/ */

View File

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

View File

@@ -19,22 +19,6 @@ components:
type: string type: string
type: type:
type: string 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: paths:
/1.0/doc-store/stored-object/create: /1.0/doc-store/stored-object/create:
@@ -85,30 +69,30 @@ paths:
- storedobject - storedobject
summary: Get a signed route to get a stored object summary: Get a signed route to get a stored object
parameters: parameters:
- in: path - in: path
name: uuid name: uuid
required: true required: true
allowEmptyValue: false allowEmptyValue: false
description: The UUID of the storedObjeect description: The UUID of the storedObjeect
schema: schema:
type: string type: string
format: uuid format: uuid
- in: path - in: path
name: method name: method
required: true required: true
allowEmptyValue: false allowEmptyValue: false
description: the method of the signed url (get or head) description: the method of the signed url (get or head)
schema: schema:
type: string type: string
enum: [ get, head ] enum: [get, head]
- in: query - in: query
name: version name: version
required: false required: false
allowEmptyValue: false allowEmptyValue: false
description: the version's filename of the stored object description: the version's filename of the stored object
schema: schema:
type: string type: string
minLength: 2 minLength: 2
responses: responses:
200: 200:
description: "OK" description: "OK"
@@ -127,14 +111,14 @@ paths:
- storedobject - storedobject
summary: Get a signed route to post stored object summary: Get a signed route to post stored object
parameters: parameters:
- in: path - in: path
name: uuid name: uuid
required: true required: true
allowEmptyValue: false allowEmptyValue: false
description: The UUID of the storedObjeect description: The UUID of the storedObjeect
schema: schema:
type: string type: string
format: uuid format: uuid
responses: responses:
200: 200:
description: "OK" description: "OK"
@@ -153,13 +137,13 @@ paths:
- storedobject - storedobject
summary: Restore an old version of a stored object summary: Restore an old version of a stored object
parameters: parameters:
- in: path - in: path
name: id name: id
required: true required: true
allowEmptyValue: false allowEmptyValue: false
description: The id of the stored object version description: The id of the stored object version
schema: schema:
type: integer type: integer
responses: responses:
200: 200:
description: "OK" description: "OK"
@@ -167,32 +151,4 @@ paths:
application/json: application/json:
schema: schema:
type: object 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: arguments:
$providersForAccompanyingPeriod: !tagged_iterator chill_doc_store.generic_doc_accompanying_period_provider $providersForAccompanyingPeriod: !tagged_iterator chill_doc_store.generic_doc_accompanying_period_provider
$providersForPerson: !tagged_iterator chill_doc_store.generic_doc_person_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: ~ Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension: ~
@@ -48,9 +44,6 @@ services:
Chill\DocStoreBundle\GenericDoc\Renderer\: Chill\DocStoreBundle\GenericDoc\Renderer\:
resource: '../GenericDoc/Renderer/' resource: '../GenericDoc/Renderer/'
Chill\DocStoreBundle\GenericDoc\Normalizer\:
resource: '../GenericDoc/Normalizer/'
Chill\DocStoreBundle\Validator\: Chill\DocStoreBundle\Validator\:
resource: '../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é shared_doc: Document partagé
title: Document partagé title: Document partagé
main_document: Document principal main_document: Document principal
attachment: Pièce jointe
# ROLES # ROLES
accompanyingCourseDocument: Documents dans les parcours d'accompagnement 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: Voir les documents
CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS: Voir les détails d'un document CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS: Voir les détails d'un document
CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE: Modifier un document CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE: Modifier un document
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\MenuCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\ShortMessageCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass; use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass; 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 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 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 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[] = [ $templateData[] = [
'template' => $this->notificationHandlerManager->getTemplate($notification), 'template' => $this->notificationHandlerManager->getTemplate($notification),
'template_data' => $this->notificationHandlerManager->getTemplateData($notification), 'template_data' => $this->notificationHandlerManager->getTemplateData($notification),
'handler' => $this->notificationHandlerManager->getHandler($notification),
'notification' => $notification, 'notification' => $notification,
]; ];
} }

View File

@@ -95,7 +95,6 @@ class UserController extends CRUDController
return $this->render('@ChillMain/User/edit.html.twig', [ return $this->render('@ChillMain/User/edit.html.twig', [
'entity' => $user, 'entity' => $user,
'access_permissions_group_list' => $this->parameterBag->get('chill_main.access_permissions_group_list'), '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(), 'add_groupcenter_form' => $this->createAddLinkGroupCenterForm($user, $request)->createView(),
'delete_groupcenter_form' => array_map( 'delete_groupcenter_form' => array_map(
static fn (Form $form) => $form->createView(), 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, 'entity_workflow' => $entityWorkflow,
'transition_form_errors' => $errors, 'transition_form_errors' => $errors,
'signatures' => $signatures, '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) public function prepend(ContainerBuilder $container)
{ {
$this->prependNotifierTexterWithLegacyData($container);
// add installation_name and date_format to globals // add installation_name and date_format to globals
$chillMainConfig = $container->getExtensionConfig($this->getAlias()); $chillMainConfig = $container->getExtensionConfig($this->getAlias());
$config = $this->processConfiguration($this $config = $this->processConfiguration($this
@@ -359,55 +357,6 @@ class ChillMainExtension extends Extension implements
// Note: the controller are loaded inside compiler pass // 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) protected function prependCruds(ContainerBuilder $container)
{ {
$container->prependExtensionConfig('chill_main', [ $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()
->end() ->end()
->arrayNode('short_messages') ->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() ->canBeEnabled()
->children() ->children()
->scalarNode('dsn')->cannotBeEmpty()->defaultValue('null://null') ->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)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
private string $workflowName; private string $workflowName;
/**
* @var Collection<int, EntityWorkflowAttachment>
*/
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowAttachment::class, cascade: ['remove'], orphanRemoval: true)]
private Collection $attachments;
public function __construct() public function __construct()
{ {
$this->subscriberToFinal = new ArrayCollection(); $this->subscriberToFinal = new ArrayCollection();
$this->subscriberToStep = new ArrayCollection(); $this->subscriberToStep = new ArrayCollection();
$this->comments = new ArrayCollection(); $this->comments = new ArrayCollection();
$this->steps = new ArrayCollection(); $this->steps = new ArrayCollection();
$this->attachments = new ArrayCollection();
$initialStep = new EntityWorkflowStep(); $initialStep = new EntityWorkflowStep();
$initialStep $initialStep
@@ -149,35 +142,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return $this; 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 public function getComments(): Collection
{ {
return $this->comments; return $this->comments;
@@ -392,17 +356,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return $this->getCurrentStep()->isOnHoldByUser($user); 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 public function isUserSubscribedToFinal(User $user): bool
{ {
return $this->subscriberToFinal->contains($user); 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 * @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 Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Doctrine\ORM\Mapping as ORM; 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\Entity]
#[ORM\Table('chill_main_workflow_entity_comment')] #[ORM\Table('chill_main_workflow_entity_comment')]
class EntityWorkflowComment implements TrackCreationInterface, TrackUpdateInterface 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\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; 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\Entity]
#[ORM\Table('chill_main_workflow_entity_step')] #[ORM\Table('chill_main_workflow_entity_step')]
class EntityWorkflowStep class EntityWorkflowStep
@@ -89,11 +80,6 @@ class EntityWorkflowStep
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null; 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])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
private bool $isFinal = false; private bool $isFinal = false;
@@ -268,11 +254,6 @@ class EntityWorkflowStep
return $this->ccUser; 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 public function getComment(): string
{ {
return $this->comment; return $this->comment;
@@ -365,9 +346,6 @@ class EntityWorkflowStep
return $this->transitionByEmail; return $this->transitionByEmail;
} }
/**
* @return bool true if this is the end of the EntityWorkflow
*/
public function isFinal(): bool public function isFinal(): bool
{ {
return $this->isFinal; return $this->isFinal;
@@ -389,9 +367,6 @@ class EntityWorkflowStep
return false; return false;
} }
/**
* @return bool if the EntityWorkflowStep is waiting for a transition, and is not the final step
*/
public function isWaitingForTransition(): bool public function isWaitingForTransition(): bool
{ {
if (null !== $this->transitionAfter) { if (null !== $this->transitionAfter) {
@@ -531,6 +506,26 @@ class EntityWorkflowStep
return $this->holdsOnStep; 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 public function addOnHold(EntityWorkflowStepHold $onHold): self
{ {
if (!$this->holdsOnStep->contains($onHold)) { if (!$this->holdsOnStep->contains($onHold)) {

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Notification; namespace Chill\MainBundle\Notification;
use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\Notification;
use Symfony\Contracts\Translation\TranslatableInterface;
interface NotificationHandlerInterface interface NotificationHandlerInterface
{ {
@@ -30,13 +29,4 @@ interface NotificationHandlerInterface
* Return true if the handler supports the handling for this notification. * Return true if the handler supports the handling for this notification.
*/ */
public function supports(Notification $notification, array $options = []): bool; 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\Entity\Notification;
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound; use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
use Doctrine\ORM\EntityManagerInterface;
final readonly class NotificationHandlerManager 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 * @throw NotificationHandlerNotFound if handler is not found

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