Compare commits

..

1 Commits

Author SHA1 Message Date
527285bb13 Remove redundant line to create edit_form 2025-01-20 12:31:38 +01:00
173 changed files with 1275 additions and 4528 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

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

4
.env
View File

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

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ Welcome to Chill documentation!
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:
@@ -42,7 +42,7 @@ Contribute
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>`_
@@ -55,11 +55,12 @@ Available bundles
* Chill Person, to deal with persons,
* chill custom fields, to add custom fields to some entities,
* 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 docs store: to store documents to people, but also entities,
* chill task: to register task with people,
* chill third party: to register third parties,
* chill family members: to register family members
You will also found the following projects :

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Service\GenericDoc\Normalizer;
use Chill\ActivityBundle\Service\GenericDoc\Providers\AccompanyingPeriodActivityGenericDocProvider;
use Chill\ActivityBundle\Service\GenericDoc\Renderers\AccompanyingPeriodActivityGenericDocRenderer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
final readonly class AccompanyingPeriodActivityGenericDocNormalizer implements GenericDocNormalizerInterface
{
public function __construct(
private StoredObjectRepositoryInterface $storedObjectRepository,
private AccompanyingPeriodActivityGenericDocRenderer $renderer,
private Environment $twig,
private TranslatorInterface $translator,
) {}
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
return AccompanyingPeriodActivityGenericDocProvider::KEY === $genericDocDTO->key
&& 'json' == $format;
}
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
$storedObject = $this->storedObjectRepository->find($genericDocDTO->identifiers['id']);
if (null === $storedObject) {
return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false];
}
return [
'isPresent' => true,
'title' => $storedObject->getTitle(),
'html' => $this->twig->render(
$this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
$this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
),
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,51 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\CalendarBundle\Service\GenericDoc\Normalizer;
use Chill\CalendarBundle\Repository\CalendarDocRepositoryInterface;
use Chill\CalendarBundle\Service\GenericDoc\Providers\AccompanyingPeriodCalendarGenericDocProvider;
use Chill\CalendarBundle\Service\GenericDoc\Renderers\AccompanyingPeriodCalendarGenericDocRenderer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
final readonly class AccompanyingPeriodCalendarGenericDocNormalizer implements GenericDocNormalizerInterface
{
public function __construct(
private AccompanyingPeriodCalendarGenericDocRenderer $renderer,
private CalendarDocRepositoryInterface $calendarDocRepository,
private Environment $twig,
private TranslatorInterface $translator,
) {}
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
return AccompanyingPeriodCalendarGenericDocProvider::KEY === $genericDocDTO->key && 'json' === $format;
}
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
if (null === $calendarDoc = $this->calendarDocRepository->find($genericDocDTO->identifiers['id'])) {
return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false];
}
return [
'isPresent' => true,
'title' => $calendarDoc->getStoredObject()->getTitle(),
'html' => $this->twig->render(
$this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
$this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true])
),
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,82 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Tests\Controller;
use Chill\DocStoreBundle\Controller\GenericDocForAccompanyingPeriodListApiController;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\MainBundle\Pagination\Paginator;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
/**
* @internal
*
* @coversNothing
*/
class GenericDocForAccompanyingPeriodListApiControllerTest extends TestCase
{
public function testSmokeTest(): void
{
$accompanyingPeriod = new AccompanyingPeriod();
$docs = [
new GenericDocDTO('dummy', ['id' => 9], new \DateTimeImmutable('2024-08-01'), $accompanyingPeriod),
new GenericDocDTO('dummy', ['id' => 1], new \DateTimeImmutable('2024-09-01'), $accompanyingPeriod),
];
$manager = $this->createMock(ManagerInterface::class);
$manager->method('findDocForAccompanyingPeriod')->with($accompanyingPeriod)->willReturn($docs);
$manager->method('countDocForAccompanyingPeriod')->with($accompanyingPeriod)->willReturn(2);
$paginatorFactory = $this->createMock(PaginatorFactoryInterface::class);
$paginatorFactory->method('create')->with(2)->willReturn(new Paginator(
2,
20,
1,
'/route',
[],
$this->createMock(UrlGeneratorInterface::class),
'page',
'item-per-page'
));
$serializer = $this->createMock(SerializerInterface::class);
$serializer->method('serialize')->with($this->isInstanceOf(Collection::class))->willReturn(
json_encode(['docs' => []])
);
$security = $this->createMock(Security::class);
$security->expects($this->once())->method('isGranted')
->with(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)->willReturn(true);
$controller = new GenericDocForAccompanyingPeriodListApiController(
$manager,
$security,
$paginatorFactory,
$serializer,
);
$response = $controller($accompanyingPeriod);
$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertEquals('{"docs":[]}', $response->getContent());
}
}

View File

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

View File

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

View File

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

View File

@@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Provide the list of GenericDoc for an accompanying period.
*/
final readonly class GenericDocForAccompanyingPeriodListApiController
{
public function __construct(
private ManagerInterface $manager,
private Security $security,
private PaginatorFactoryInterface $paginator,
private SerializerInterface $serializer,
) {}
#[Route('/api/1.0/doc-store/generic-doc/by-period/{id}/index', methods: ['GET'])]
public function __invoke(AccompanyingPeriod $accompanyingPeriod): JsonResponse
{
if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)) {
throw new AccessDeniedHttpException('not allowed to see the documents for accompanying period');
}
$nb = $this->manager->countDocForAccompanyingPeriod($accompanyingPeriod);
$paginator = $this->paginator->create($nb);
$docs = $this->manager->findDocForAccompanyingPeriod($accompanyingPeriod, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage());
$collection = new Collection($docs, $paginator);
return new JsonResponse(
$this->serializer->serialize($collection, 'json', [AbstractNormalizer::GROUPS => ['read']]),
json: true,
);
}
}

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\GenericDoc\Exception;
class AssociatedStoredObjectNotFound extends \RuntimeException
{
public function __construct(string $key, array $identifiers, int $code = 0, ?\Throwable $previous = null)
{
parent::__construct(sprintf('No stored object found for generic doc with key "%s" and identifiers "%s"', $key, json_encode($identifiers)), $code, $previous);
}
}

View File

@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\GenericDoc\Exception;
class NotNormalizableGenericDocException extends \LogicException {}

View File

@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\GenericDoc\Exception;
class UnexpectedValueException extends \UnexpectedValueException {}

View File

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

View File

@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\GenericDoc;
/**
* Normalize a Generic Doc.
*/
interface GenericDocNormalizerInterface
{
/**
* Return true if a generic doc can be normalized by this implementation.
*/
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool;
/**
* Normalize a generic doc into an array.
*
* @return array{title: string, html?: string, isPresent: bool}
*/
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array;
}

View File

@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\GenericDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
interface GenericDocProviderInterface
{
public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject;
/**
* Return true if this provider supports the given Generic doc for various informations.
*
* Concerned:
*
* - @see{self::fetchAssociatedStoredObject}
*/
public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool;
/**
* return true if the implementation supports key and identifiers.
*/
public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool;
/**
* Build a GenericDocDTO, given the key and identifiers.
*/
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO;
}

View File

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

View File

@@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\GenericDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\Exception\AssociatedStoredObjectNotFound;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\DBAL\Exception;
interface ManagerInterface
{
/**
* @param list<string> $places
*
* @throws Exception
*/
public function countDocForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): int;
public function countDocForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): int;
/**
* @param list<string> $places places to search. When empty, search in all places
*
* @return iterable<GenericDocDTO>
*
* @throws Exception
*/
public function findDocForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, int $offset = 0, int $limit = 20, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): iterable;
/**
* @param list<string> $places places to search. When empty, search in all places
*
* @return iterable<GenericDocDTO>
*/
public function findDocForPerson(Person $person, int $offset = 0, int $limit = 20, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): iterable;
public function placesForPerson(Person $person): array;
public function placesForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): array;
public function isGenericDocNormalizable(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool;
/**
* @return array{title: string, html?: string}
*/
public function normalizeGenericDoc(GenericDocDTO $genericDocDTO, string $format, array $context = []): array;
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO;
/**
* @throws AssociatedStoredObjectNotFound if no stored object can be found
*/
public function fetchStoredObject(GenericDocDTO $genericDocDTO): StoredObject;
}

View File

@@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\GenericDoc\Normalizer;
use Chill\DocStoreBundle\GenericDoc\Exception\UnexpectedValueException;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericDocProvider;
use Chill\DocStoreBundle\GenericDoc\Renderer\AccompanyingCourseDocumentGenericDocRenderer;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Twig\Environment;
class AccompanyingCourseDocumentGenericDocNormalizer implements GenericDocNormalizerInterface
{
public function __construct(
private readonly AccompanyingCourseDocumentRepository $repository,
private readonly Environment $twig,
private readonly AccompanyingCourseDocumentGenericDocRenderer $renderer,
) {}
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
return AccompanyingCourseDocumentGenericDocProvider::KEY === $genericDocDTO->key;
}
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
if (!array_key_exists('id', $genericDocDTO->identifiers)) {
throw new UnexpectedValueException('key id not found in identifier');
}
$document = $this->repository->find($genericDocDTO->identifiers['id']);
if (null === $document) {
throw new UnexpectedValueException('document not found with id '.$genericDocDTO->identifiers['id']);
}
return [
'isPresent' => true,
'title' => $document->getTitle(),
'html' => $this->twig->render(
$this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
$this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true])
),
];
}
}

View File

@@ -1,51 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\GenericDoc\Normalizer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
use Chill\DocStoreBundle\GenericDoc\Renderer\AccompanyingCourseDocumentGenericDocRenderer;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
final readonly class PersonDocumentGenericDocNormalizer implements GenericDocNormalizerInterface
{
public function __construct(
private PersonDocumentRepository $personDocumentRepository,
private AccompanyingCourseDocumentGenericDocRenderer $renderer,
private Environment $twig,
private TranslatorInterface $translator,
) {}
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
return PersonDocumentGenericDocProvider::KEY === $genericDocDTO->key && 'json' === $format;
}
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
if (null === $personDocument = $this->personDocumentRepository->find($genericDocDTO->identifiers['id'])) {
return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false];
}
return [
'isPresent' => true,
'title' => $personDocument->getTitle(),
'html' => $this->twig->render(
$this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
$this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true])
),
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Tests\Serializer\Normalizer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
use Chill\DocStoreBundle\Serializer\Normalizer\GenericDocNormalizer;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
*
* @coversNothing
*/
class GenericDocNormalizerTest extends TestCase
{
private $normalizer;
private ManagerInterface $manager;
protected function setUp(): void
{
$this->manager = $this->createMock(ManagerInterface::class);
$this->normalizer = new GenericDocNormalizer($this->manager);
$innerNormalizer = $this->createMock(NormalizerInterface::class);
$innerNormalizer->method('normalize')
->willReturnCallback(fn ($date) => $date instanceof \DateTimeImmutable ? $date->format(DATE_ATOM) : null);
$this->normalizer->setNormalizer($innerNormalizer);
}
public function testNormalize()
{
$docDate = new \DateTimeImmutable('2023-10-01T15:03:01.012345Z');
$object = new GenericDocDTO(
'some_key',
['id' => 'id1', 'other_id' => 'id2'],
$docDate,
new AccompanyingPeriod(),
);
$expected = [
'type' => 'doc_store_generic_doc',
'key' => 'some_key',
'identifiers' => ['id' => 'id1', 'other_id' => 'id2'],
'context' => 'accompanying-period',
'doc_date' => $docDate->format(DATE_ATOM),
'uniqueKey' => 'some_keyidother_idid1id2',
'metadata' => [],
'storedObject' => null,
];
$this->manager->expects($this->once())->method('isGenericDocNormalizable')
->with($object, 'json', [])
->willReturn(true);
$actual = $this->normalizer->normalize($object, 'json', []);
$this->assertEquals($expected, $actual);
}
}

View File

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

View File

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

View File

@@ -19,22 +19,6 @@ components:
type: string
type:
type: string
GenericDoc:
type: object
properties:
type:
type: string
enum:
- doc_store_generic_doc
key:
type: string
context:
type: string
enum:
- person
- accompanying-period
doc_date:
$ref: '#/components/schemas/Date'
paths:
/1.0/doc-store/stored-object/create:
@@ -85,30 +69,30 @@ paths:
- storedobject
summary: Get a signed route to get a stored object
parameters:
- in: path
name: uuid
required: true
allowEmptyValue: false
description: The UUID of the storedObjeect
schema:
type: string
format: uuid
- in: path
name: method
required: true
allowEmptyValue: false
description: the method of the signed url (get or head)
schema:
type: string
enum: [ get, head ]
- in: query
name: version
required: false
allowEmptyValue: false
description: the version's filename of the stored object
schema:
type: string
minLength: 2
- in: path
name: uuid
required: true
allowEmptyValue: false
description: The UUID of the storedObjeect
schema:
type: string
format: uuid
- in: path
name: method
required: true
allowEmptyValue: false
description: the method of the signed url (get or head)
schema:
type: string
enum: [get, head]
- in: query
name: version
required: false
allowEmptyValue: false
description: the version's filename of the stored object
schema:
type: string
minLength: 2
responses:
200:
description: "OK"
@@ -127,14 +111,14 @@ paths:
- storedobject
summary: Get a signed route to post stored object
parameters:
- in: path
name: uuid
required: true
allowEmptyValue: false
description: The UUID of the storedObjeect
schema:
type: string
format: uuid
- in: path
name: uuid
required: true
allowEmptyValue: false
description: The UUID of the storedObjeect
schema:
type: string
format: uuid
responses:
200:
description: "OK"
@@ -153,13 +137,13 @@ paths:
- storedobject
summary: Restore an old version of a stored object
parameters:
- in: path
name: id
required: true
allowEmptyValue: false
description: The id of the stored object version
schema:
type: integer
- in: path
name: id
required: true
allowEmptyValue: false
description: The id of the stored object version
schema:
type: integer
responses:
200:
description: "OK"
@@ -167,32 +151,4 @@ paths:
application/json:
schema:
type: object
/1.0/doc-store/generic-doc/by-period/{id}/index:
get:
tags:
- storedobject
summary: A list of generic doc associated with the accompanying period
parameters:
- in: path
name: id
required: true
allowEmptyValue: false
description: The id of the accompanying period
schema:
type: integer
responses:
200:
description: "OK"
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/PaginatedResult'
- type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/GenericDoc'
type: object

View File

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

View File

@@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\DocStore;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20241212112733 extends AbstractMigration
{
public function getDescription(): string
{
return 'Move the title of PersonDocument and AccompanyingCourseDocument to stored object';
}
public function up(Schema $schema): void
{
$this->addSql('UPDATE chill_doc.stored_object SET title = ac_doc.title FROM chill_doc.accompanyingcourse_document ac_doc WHERE ac_doc.object_id = stored_object.id');
$this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document DROP scope_id');
$this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document DROP title');
$this->addSql('UPDATE chill_doc.stored_object SET title = p_doc.title FROM chill_doc.person_document p_doc WHERE p_doc.object_id = stored_object.id');
$this->addSql('ALTER TABLE chill_doc.person_document DROP title');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD scope_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD title TEXT DEFAULT \'\' NOT NULL');
$this->addSql('UPDATE chill_doc.accompanyingcourse_document SET title = so.title FROM chill_doc.stored_object so WHERE object_id = so.id');
$this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD CONSTRAINT fk_a45098f6682b5931 FOREIGN KEY (scope_id) REFERENCES scopes (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX idx_a45098f6682b5931 ON chill_doc.accompanyingcourse_document (scope_id)');
$this->addSql('ALTER TABLE chill_doc.person_document ADD title TEXT DEFAULT \'\' NOT NULL');
$this->addSql('UPDATE chill_doc.person_document SET title = so.title FROM chill_doc.stored_object so WHERE object_id = so.id');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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\MainBundle\Repository\Workflow;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
/**
* @implements ObjectRepository<EntityWorkflowAttachment>
*/
class EntityWorkflowAttachmentRepository implements ObjectRepository
{
private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $registry)
{
$this->repository = $registry->getRepository(EntityWorkflowAttachment::class);
}
public function find($id): ?EntityWorkflowAttachment
{
return $this->repository->find($id);
}
public function findAll(): array
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
}
/**
* @return array<EntityWorkflowAttachment>
*/
public function findByStoredObject(StoredObject $storedObject): array
{
$qb = $this->repository->createQueryBuilder('a');
$qb->where('a.proxyStoredObject = :storedObject')->setParameter('storedObject', $storedObject);
return $qb->getQuery()->getResult();
}
public function getClassName()
{
return EntityWorkflowAttachment::class;
}
}

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