diff --git a/CHANGELOG.md b/CHANGELOG.md
index edd7fbc91..a198c0929 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,14 @@ and this project adheres to
## Unreleased
+* renommer "dossier numéro" en "parcours numéro" dans les résultats de recherche
+* renomme date de début en date d'ouverture dans le formulaire parcours
+
+
+## Test releases
+
+### test release 2021-01-31
+
[fast_actions] improve fast-actions buttons override mechanism, fix https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/413
[homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
* [person] accompanying course: optimisation: do not fetch some resources for the banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/409)
@@ -26,8 +34,6 @@ and this project adheres to
* [workflow][notification] improve how notifications and workflows are 'attached' to entities: contextual list, counter, buttons and vue modal
-## Test releases
-
### test release 2021-01-28
* [person] improve filiations vis graph: disable physics, use chill colors for persons-households-course, increase label of relations, remove labels on household arrows and other improvements (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/286, https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/362)
diff --git a/src/Bundle/ChillActivityBundle/Entity/ActivityType.php b/src/Bundle/ChillActivityBundle/Entity/ActivityType.php
index 94f732a89..bdf75ed05 100644
--- a/src/Bundle/ChillActivityBundle/Entity/ActivityType.php
+++ b/src/Bundle/ChillActivityBundle/Entity/ActivityType.php
@@ -271,7 +271,7 @@ class ActivityType
public function checkSocialActionsVisibility(ExecutionContextInterface $context, $payload)
{
if ($this->socialIssuesVisible !== $this->socialActionsVisible) {
- if (!($this->socialIssuesVisible === 2 && $this->socialActionsVisible === 1)) {
+ if (!(2 === $this->socialIssuesVisible && 1 === $this->socialActionsVisible)) {
$context
->buildViolation('The socialActionsVisible value is not compatible with the socialIssuesVisible value')
->atPath('socialActionsVisible')
diff --git a/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityReasonAggregatorTest.php b/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityReasonAggregatorTest.php
index fb98f0a8e..436bfc697 100644
--- a/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityReasonAggregatorTest.php
+++ b/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityReasonAggregatorTest.php
@@ -9,7 +9,7 @@
declare(strict_types=1);
-namespace Chill\ActivityBundle\Tests\Aggregator;
+namespace Chill\ActivityBundle\Tests\Export\Aggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
diff --git a/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityTypeAggregatorTest.php b/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityTypeAggregatorTest.php
index 7959825a7..f6efe17a5 100644
--- a/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityTypeAggregatorTest.php
+++ b/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityTypeAggregatorTest.php
@@ -9,7 +9,7 @@
declare(strict_types=1);
-namespace Chill\ActivityBundle\Tests\Aggregator;
+namespace Chill\ActivityBundle\Tests\Export\Aggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
diff --git a/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityUserAggregatorTest.php b/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityUserAggregatorTest.php
index 056ba6049..1447f473b 100644
--- a/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityUserAggregatorTest.php
+++ b/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ActivityUserAggregatorTest.php
@@ -9,7 +9,7 @@
declare(strict_types=1);
-namespace Chill\ActivityBundle\Tests\Aggregator;
+namespace Chill\ActivityBundle\Tests\Export\Aggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
diff --git a/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ActivityReasonFilterTest.php b/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ActivityReasonFilterTest.php
index cbac6febd..5b8ae08c3 100644
--- a/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ActivityReasonFilterTest.php
+++ b/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ActivityReasonFilterTest.php
@@ -9,7 +9,7 @@
declare(strict_types=1);
-namespace Chill\ActivityBundle\Tests\Filter;
+namespace Chill\ActivityBundle\Tests\Export\Filter;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\Common\Collections\ArrayCollection;
diff --git a/src/Bundle/ChillActivityBundle/Tests/Export/Filter/PersonHavingActivityBetweenDateFilterTest.php b/src/Bundle/ChillActivityBundle/Tests/Export/Filter/PersonHavingActivityBetweenDateFilterTest.php
index f444c368b..94d99c2a7 100644
--- a/src/Bundle/ChillActivityBundle/Tests/Export/Filter/PersonHavingActivityBetweenDateFilterTest.php
+++ b/src/Bundle/ChillActivityBundle/Tests/Export/Filter/PersonHavingActivityBetweenDateFilterTest.php
@@ -9,7 +9,7 @@
declare(strict_types=1);
-namespace Chill\ActivityBundle\Tests\Filter;
+namespace Chill\ActivityBundle\Tests\Export\Filter;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use DateTime;
diff --git a/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsChoiceTest.php b/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsChoiceTest.php
index db27ec921..d3355b9ce 100644
--- a/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsChoiceTest.php
+++ b/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsChoiceTest.php
@@ -9,7 +9,7 @@
declare(strict_types=1);
-namespace Chill\CustomFieldsBundle\Tests;
+namespace Chill\CustomFieldsBundle\Tests\CustomFields;
use Chill\CustomFieldsBundle\CustomFields\CustomFieldChoice;
use Chill\CustomFieldsBundle\Entity\CustomField;
diff --git a/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsNumberTest.php b/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsNumberTest.php
index 60381f567..fb0079f05 100644
--- a/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsNumberTest.php
+++ b/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsNumberTest.php
@@ -9,7 +9,7 @@
declare(strict_types=1);
-namespace Chill\CustomFieldsBundle\Tests;
+namespace Chill\CustomFieldsBundle\Tests\CustomFields;
use Chill\CustomFieldsBundle\CustomFields\CustomFieldNumber;
use Chill\CustomFieldsBundle\Entity\CustomField;
diff --git a/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsTextTest.php b/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsTextTest.php
index 50bf689d4..c1dca44c0 100644
--- a/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsTextTest.php
+++ b/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsTextTest.php
@@ -9,10 +9,11 @@
declare(strict_types=1);
-namespace Chill\CustomFieldsBundle\Tests;
+namespace Chill\CustomFieldsBundle\Tests\CustomFields;
use Chill\CustomFieldsBundle\CustomFields\CustomFieldText;
use Chill\CustomFieldsBundle\Entity\CustomField;
+use Chill\CustomFieldsBundle\Tests\CustomFieldTestHelper;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
diff --git a/src/Bundle/ChillCustomFieldsBundle/Tests/Routing/RoutingLoaderTest.php b/src/Bundle/ChillCustomFieldsBundle/Tests/Routing/RoutingLoaderTest.php
index 3cde3890a..32c6639bc 100644
--- a/src/Bundle/ChillCustomFieldsBundle/Tests/Routing/RoutingLoaderTest.php
+++ b/src/Bundle/ChillCustomFieldsBundle/Tests/Routing/RoutingLoaderTest.php
@@ -9,7 +9,7 @@
declare(strict_types=1);
-namespace Chill\CustomFieldsBundle\Tests;
+namespace Chill\CustomFieldsBundle\Tests\Routing;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;
diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php
index 33f1bd774..247cdf552 100644
--- a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php
+++ b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php
@@ -19,6 +19,7 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
+use Chill\MainBundle\Workflow\Validator\StepDestValid;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -201,9 +202,18 @@ class WorkflowController extends AbstractController
$entityWorkflow->getCurrentStep()->addDestUser($user);
}
- $this->entityManager->flush();
+ $errors = $this->validator->validate(
+ $entityWorkflow->getCurrentStep(),
+ new StepDestValid()
+ );
- return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
+ if (count($errors) === 0) {
+ $this->entityManager->flush();
+
+ return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
+ }
+
+ return new Response((string) $errors, Response::HTTP_UNPROCESSABLE_ENTITY);
}
if ($transitionForm->isSubmitted() && !$transitionForm->isValid()) {
@@ -235,6 +245,7 @@ class WorkflowController extends AbstractController
'handler_template_data' => $handler->getTemplateData($entityWorkflow),
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
'entity_workflow' => $entityWorkflow,
+ 'transition_form_errors' => $errors ?? [],
//'comment_form' => $commentForm->createView(),
]
);
diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php
index c7a322338..a132706d7 100644
--- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php
+++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php
@@ -174,6 +174,20 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return null;
}
+ public function getCurrentStepChained(): ?EntityWorkflowStep
+ {
+ $steps = $this->getStepsChained();
+ $currentStep = $this->getCurrentStep();
+
+ foreach ($steps as $step) {
+ if ($step === $currentStep) {
+ return $step;
+ }
+ }
+
+ return null;
+ }
+
public function getCurrentStepCreatedAt(): ?DateTimeInterface
{
if (null !== $previous = $this->getPreviousStepIfAny()) {
diff --git a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php
index 18ac10c47..eaca35812 100644
--- a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php
+++ b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php
@@ -48,6 +48,8 @@ class WorkflowStepType extends AbstractType
$entityWorkflow = $options['entity_workflow'];
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
+ $place = $workflow->getMarking($entityWorkflow);
+ $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]);
if (true === $options['transition']) {
if (null === $options['entity_workflow']) {
@@ -68,40 +70,77 @@ class WorkflowStepType extends AbstractType
$transitions
);
+ if (array_key_exists('validationFilterInputLabels', $placeMetadata)) {
+ $inputLabels = $placeMetadata['validationFilterInputLabels'];
+
+ $builder->add('transitionFilter', ChoiceType::class, [
+ 'multiple' => false,
+ 'label' => 'workflow.My decision',
+ 'choices' => [
+ 'forward' => 'forward',
+ 'backward' => 'backward',
+ 'neutral' => 'neutral',
+ ],
+ 'choice_label' => function (string $key) use ($inputLabels) {
+ return $this->translatableStringHelper->localize($inputLabels[$key]);
+ },
+ 'choice_attr' => static function (string $key) {
+ return [
+ $key => $key,
+ ];
+ },
+ 'mapped' => false,
+ 'expanded' => true,
+ 'data' => 'forward',
+ ]);
+ }
+
$builder
->add('transition', ChoiceType::class, [
- 'label' => 'workflow.Transition to apply',
+ 'label' => 'workflow.Next step',
'mapped' => false,
'multiple' => false,
'expanded' => true,
'choices' => $choices,
'choice_label' => function (Transition $transition) use ($workflow) {
- $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
+ $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
- if (array_key_exists('label', $meta)) {
- return $this->translatableStringHelper->localize($meta['label']);
- }
+ if (array_key_exists('label', $meta)) {
+ return $this->translatableStringHelper->localize($meta['label']);
+ }
- return $transition->getName();
- },
+ return $transition->getName();
+ },
'choice_attr' => static function (Transition $transition) use ($workflow) {
- $toFinal = true;
+ $toFinal = true;
+ $isForward = 'neutral';
- foreach ($transition->getTos() as $to) {
- $meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
+ $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
- if (
+ if (array_key_exists('isForward', $metadata)) {
+ if ($metadata['isForward']) {
+ $isForward = 'forward';
+ } else {
+ $isForward = 'backward';
+ }
+ }
+
+ foreach ($transition->getTos() as $to) {
+ $meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
+
+ if (
!array_key_exists('isFinal', $meta) || false === $meta['isFinal']
) {
- $toFinal = false;
- }
+ $toFinal = false;
}
+ }
- return [
- 'data-is-transition' => 'data-is-transition',
- 'data-to-final' => $toFinal ? '1' : '0',
- ];
- },
+ return [
+ 'data-is-transition' => 'data-is-transition',
+ 'data-to-final' => $toFinal ? '1' : '0',
+ 'data-is-forward' => $isForward,
+ ];
+ },
])
->add('future_dest_users', PickUserDynamicType::class, [
'label' => 'workflow.dest for next steps',
diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/show_hide/show_hide.js b/src/Bundle/ChillMainBundle/Resources/public/lib/show_hide/show_hide.js
index f2bcc9b45..25307a120 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/lib/show_hide/show_hide.js
+++ b/src/Bundle/ChillMainBundle/Resources/public/lib/show_hide/show_hide.js
@@ -1,19 +1,19 @@
/**
* Create a control to show or hide values
- *
+ *
* Possible options are:
- *
+ *
* - froms: an Element, an Array of Element, or a NodeList. A
* listener will be attached to **all** input of those elements
* and will trigger the check on changes
- * - test: a function which will test the element and will return true
+ * - test: a function which will test the element and will return true
* if the content must be shown, false if it must be hidden.
- * The function will receive the `froms` as first argument, and the
+ * The function will receive the `froms` as first argument, and the
* event as second argument.
- * - container: an Element, an Array of Element, or a Node List. The
+ * - container: an Element, an Array of Element, or a Node List. The
* child nodes will be hidden / shown inside this container
* - event_name: the name of the event to listen to. `'change'` by default.
- *
+ *
* @param object options
*/
var ShowHide = function(options) {
@@ -26,8 +26,10 @@ var ShowHide = function(options) {
container_content = [],
debug = 'debug' in options ? options.debug : false,
load_event = 'load_event' in options ? options.load_event : 'load',
- id = 'uid' in options ? options.id : Math.random();
-
+ id = 'uid' in options ? options.id : Math.random(),
+ toggle_callback = 'toggle_callback' in options ? options.toggle_callback : null
+ ;
+
var bootstrap = function(event) {
if (debug) {
console.log('debug is activated on this show-hide', this);
@@ -39,15 +41,14 @@ var ShowHide = function(options) {
contents.push(el);
}
container_content.push(contents);
- // console.log('container content', container_content);
}
// attach the listener on each input
for (let f of froms.values()) {
- let
- inputs = f.querySelectorAll('input'),
+ let
+ inputs = f.querySelectorAll('input'),
selects = f.querySelectorAll('select');
-
+
for (let input of inputs.values()) {
if (debug) {
console.log('attaching event to input', input);
@@ -67,10 +68,10 @@ var ShowHide = function(options) {
}
// first launch of the show/hide
- onChange(event);
+ onChange(event);
};
-
+
var onChange = function (event) {
var result = test(froms, event), me;
@@ -89,45 +90,53 @@ var ShowHide = function(options) {
} else {
throw "the result of test is not a boolean";
}
-
+
};
-
+
var forceHide = function() {
if (debug) {
console.log('force hide');
}
- for (let contents of container_content.values()) {
- for (let el of contents.values()) {
- el.remove();
+ if (toggle_callback !== null) {
+ toggle_callback(container, 'hide');
+ } else {
+ for (let contents of container_content.values()) {
+ for (let el of contents.values()) {
+ el.remove();
+ }
}
}
is_shown = false;
};
-
+
var forceShow = function() {
if (debug) {
console.log('show');
}
- for (let i of container_content.keys()) {
- var contents = container_content[i];
- for (let el of contents.values()) {
- container[i].appendChild(el);
+ if (toggle_callback !== null) {
+ toggle_callback(container, 'show');
+ } else {
+ for (let i of container_content.keys()) {
+ var contents = container_content[i];
+ for (let el of contents.values()) {
+ container[i].appendChild(el);
+ }
}
}
is_shown = true;
};
-
+
var forceCompute = function(event) {
onChange(event);
};
-
-
+
+
if (load_event !== null) {
window.addEventListener('load', bootstrap);
} else {
bootstrap(null);
}
-
+
return {
forceHide: forceHide,
forceShow: forceShow,
diff --git a/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js b/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js
index 2e2d4e89c..bd05a8aaa 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js
+++ b/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js
@@ -2,29 +2,89 @@ import {ShowHide} from 'ChillMainAssets/lib/show_hide/show_hide.js';
window.addEventListener('DOMContentLoaded', function() {
let
- finalizeAfterContainer = document.querySelector('#finalizeAfter'),
+ divTransitions = document.querySelector('#transitions'),
futureDestUsersContainer = document.querySelector('#futureDestUsers')
;
- if (null === finalizeAfterContainer) {
- return;
- }
-
- new ShowHide({
- load_event: null,
- froms: [finalizeAfterContainer],
- container: [futureDestUsersContainer],
- test: function(containers, arg2, arg3) {
- for (let container of containers) {
- for (let input of container.querySelectorAll('input')) {
- if (!input.checked) {
- return true;
- } else {
- return false;
+ if (null !== divTransitions) {
+ new ShowHide({
+ load_event: null,
+ froms: [divTransitions],
+ container: [futureDestUsersContainer],
+ test: function(divs, arg2, arg3) {
+ for (let div of divs) {
+ for (let input of div.querySelectorAll('input')) {
+ if (input.checked) {
+ if (input.dataset.toFinal === "1") {
+ return false;
+ } else {
+ return true;
+ }
+ }
}
}
- }
- },
- })
+ return true;
+ },
+ });
+ }
+
+ let
+ transitionFilterContainer = document.querySelector('#transitionFilter'),
+ transitions = document.querySelector('#transitions')
+ ;
+
+ if (null !== transitionFilterContainer) {
+ transitions.querySelectorAll('.form-check').forEach(function(row) {
+
+ const isForward = row.querySelector('input').dataset.isForward;
+
+ new ShowHide({
+ load_event: null,
+ froms: [transitionFilterContainer],
+ container: row,
+ test: function (containers) {
+ for (let container of containers) {
+ for (let input of container.querySelectorAll('input')) {
+ if (input.checked) {
+ return isForward === input.value;
+ }
+ }
+ }
+ },
+ toggle_callback: function (c, dir) {
+ for (let div of c) {
+ let input = div.querySelector('input');
+ if ('hide' === dir) {
+ input.checked = false;
+ input.disabled = true;
+ } else {
+ input.disabled = false;
+ }
+ }
+ },
+ });
+ });
+ }
+
+ // validate form
+ let form = document.querySelector('form[name="workflow_step"]');
+
+ if (form === null) {
+ console.error('form to validate not found');
+ }
+
+ form.addEventListener('submit', function (event) {
+ const datas = new FormData(event.target);
+
+ if (datas.has('workflow_step[future_dest_users]')) {
+ const dests = JSON.parse(datas.get('workflow_step[future_dest_users]'));
+ if (dests === null || (dests instanceof Array && dests.length === 0)) {
+ event.preventDefault();
+ console.log('no users!');
+ window.alert('Indiquez un utilisateur pour traiter la prochaine étape.');
+ }
+ }
+ });
+
});
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue
index a188a3a02..2e4125c1a 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue
@@ -1,5 +1,5 @@
-
+
-
+
{{ $t('workflow_list') }}
-
+
+ {{ step.previous.comment|chill_markdown_to_html }} ++
{{ 'workflow.Users allowed to apply transition'|trans }} :
+