mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge remote-tracking branch 'ChillTask/master'
This commit is contained in:
commit
4c731a55f7
1
src/Bundle/ChillTask/.gitignore
vendored
Normal file
1
src/Bundle/ChillTask/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/vendor/
|
25
src/Bundle/ChillTask/.gitlab-ci.yml
Normal file
25
src/Bundle/ChillTask/.gitlab-ci.yml
Normal file
@ -0,0 +1,25 @@
|
||||
.test_definition: &test_definition
|
||||
services:
|
||||
- chill/database:latest
|
||||
before_script:
|
||||
- composer config github-oauth.github.com $GITHUB_TOKEN
|
||||
- composer install
|
||||
- cp Resources/test/Fixtures/App/app/config/parameters.gitlab-ci.yml Resources/test/Fixtures/App/app/config/parameters.yml
|
||||
- php Resources/test/Fixtures/App/app/console --env=test cache:warmup
|
||||
- php Resources/test/Fixtures/App/app/console doctrine:migrations:migrate --env=test --no-interaction
|
||||
- php Resources/test/Fixtures/App/app/console doctrine:fixtures:load --env=test --no-interaction
|
||||
|
||||
|
||||
stages:
|
||||
- deploy
|
||||
|
||||
deploy-packagist:
|
||||
stage: deploy
|
||||
image: chill/ci-image:php-7.2
|
||||
before_script:
|
||||
# test that PACKAGIST USERNAME and PACKAGIST_TOKEN variable are set
|
||||
- if [ -z ${PACKAGIST_USERNAME+x} ]; then echo "Please set PACKAGIST_USERNAME variable"; exit -1; fi
|
||||
- if [ -z ${PACKAGIST_TOKEN+x} ]; then echo "Please set PACKAGIST_TOKEN variable"; exit -1; fi
|
||||
script:
|
||||
- STATUSCODE=$(curl -XPOST -H'content-type:application/json' "https://packagist.org/api/update-package?username=$PACKAGIST_USERNAME&apiToken=$PACKAGIST_TOKEN" -d"{\"repository\":{\"url\":\"$CI_PROJECT_URL.git\"}}" --silent --output /dev/stderr --write-out "%{http_code}")
|
||||
- if [ $STATUSCODE = "202" ]; then exit 0; else exit $STATUSCODE; fi
|
52
src/Bundle/ChillTask/CHANGELOG.md
Normal file
52
src/Bundle/ChillTask/CHANGELOG.md
Normal file
@ -0,0 +1,52 @@
|
||||
Version 1.5.1
|
||||
=============
|
||||
|
||||
- fix bug in filter task form: allow to show the list of users, which was hidden when the user had access to multiple centers;
|
||||
- add assignee in task list;
|
||||
- fix some translation;
|
||||
- add a filtering by center on list;
|
||||
- add color in boxes for task statuses;
|
||||
|
||||
Version 1.5.4
|
||||
=============
|
||||
|
||||
- adding indexes on place event and task
|
||||
|
||||
Version 1.5.5
|
||||
=============
|
||||
|
||||
- Fix error on the "see more" link which was not showed
|
||||
- Layout of the task list
|
||||
|
||||
Version 1.5.6
|
||||
=============
|
||||
|
||||
- fix: validation error on warning date interval is not shown
|
||||
- add privacy events to task show / list;
|
||||
- add privacy events to task edit / update;
|
||||
|
||||
Version 1.5.7
|
||||
==============
|
||||
|
||||
- fix error when showing task list without person in context (issue #3) ;
|
||||
|
||||
Version 1.5.8
|
||||
=============
|
||||
|
||||
- add returnPath to page Show and List for Single tasks ;
|
||||
|
||||
Version 1.5.9
|
||||
=============
|
||||
|
||||
- better exception description when task workflow is not found ;
|
||||
|
||||
Version 1.5.10
|
||||
==============
|
||||
|
||||
- load webpack config using a `configure` function ;
|
||||
|
||||
Version 1.5.11
|
||||
==============
|
||||
|
||||
- [task] fix loading of chill task list ;
|
||||
|
36
src/Bundle/ChillTask/ChillTaskBundle.php
Normal file
36
src/Bundle/ChillTask/ChillTaskBundle.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Chill\TaskBundle\DependencyInjection\Compiler\TaskWorkflowDefinitionCompilerPass;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
class ChillTaskBundle extends Bundle
|
||||
{
|
||||
public function build(ContainerBuilder $container)
|
||||
{
|
||||
parent::build($container);
|
||||
|
||||
$container->addCompilerPass(new TaskWorkflowDefinitionCompilerPass());
|
||||
}
|
||||
}
|
604
src/Bundle/ChillTask/Controller/SingleTaskController.php
Normal file
604
src/Bundle/ChillTask/Controller/SingleTaskController.php
Normal file
@ -0,0 +1,604 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Controller;
|
||||
|
||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Chill\TaskBundle\Form\SingleTaskType;
|
||||
use Chill\TaskBundle\Form\SingleTaskListType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\TaskBundle\Repository\SingleTaskRepository;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\MainBundle\Entity\UserRepository;
|
||||
use Chill\TaskBundle\Event\TaskEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
use Chill\TaskBundle\Event\UI\UIEvent;
|
||||
use Chill\MainBundle\Repository\CenterRepository;
|
||||
|
||||
|
||||
class SingleTaskController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
|
||||
/**
|
||||
* SingleTaskController constructor.
|
||||
*
|
||||
* @param EventDispatcherInterface $eventDispatcher
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @Route(
|
||||
* "/{_locale}/task/single-task/new",
|
||||
* name="chill_task_single_task_new"
|
||||
* )
|
||||
*/
|
||||
public function newAction(
|
||||
Request $request,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
|
||||
$task = (new SingleTask())
|
||||
->setAssignee($this->getUser())
|
||||
->setType('task_default')
|
||||
;
|
||||
|
||||
if ($request->query->has('person_id')) {
|
||||
$personId = $request->query->getInt('person_id', null);
|
||||
|
||||
if ($personId === null) {
|
||||
return new Response("You must provide a person_id", Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$person = $this->getDoctrine()->getManager()
|
||||
->getRepository(Person::class)
|
||||
->find($personId);
|
||||
|
||||
if ($person === null) {
|
||||
$this->createNotFoundException("Invalid person id");
|
||||
}
|
||||
|
||||
$task->setPerson($person);
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(TaskVoter::CREATE, $task, 'You are not '
|
||||
. 'allowed to create this task');
|
||||
|
||||
$form = $this->setCreateForm($task, new Role(TaskVoter::CREATE));
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted()) {
|
||||
if ($form->isValid()) {
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->persist($task);
|
||||
|
||||
$this->eventDispatcher->dispatch(TaskEvent::PERSIST, new TaskEvent($task));
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $translator->trans("The task is created"));
|
||||
|
||||
return $this->redirectToRoute('chill_task_singletask_list', [
|
||||
'person_id' => $task->getPerson()->getId()
|
||||
]);
|
||||
|
||||
} else {
|
||||
$this->addFlash('error', $translator->trans("This form contains errors"));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('ChillTaskBundle:SingleTask:new.html.twig', array(
|
||||
'form' => $form->createView(),
|
||||
'task' => $task
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @Route(
|
||||
* "/{_locale}/task/single-task/{id}/show",
|
||||
* name="chill_task_single_task_show"
|
||||
* )
|
||||
*/
|
||||
public function showAction(Request $request, $id)
|
||||
{
|
||||
/* @var $taskRepository SingleTaskRepository */
|
||||
$taskRepository = $this->get('chill_task.single_task_repository');
|
||||
|
||||
$task = $taskRepository->find($id);
|
||||
|
||||
if (!is_null($task->getPerson() === !null)) {
|
||||
$personId = $task->getPerson()->getId();
|
||||
|
||||
if ($personId === null) {
|
||||
return new Response("You must provide a person_id", Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$person = $this->getDoctrine()->getManager()
|
||||
->getRepository(Person::class)
|
||||
->find($personId);
|
||||
|
||||
if ($person === null) {
|
||||
throw $this->createNotFoundException("Invalid person id");
|
||||
}
|
||||
}
|
||||
$this->denyAccessUnlessGranted(TaskVoter::SHOW, $task, 'You are not '
|
||||
. 'allowed to view this task');
|
||||
|
||||
if (!$task) {
|
||||
throw $this->createNotFoundException('Unable to find Task entity.');
|
||||
}
|
||||
|
||||
$timeline = $this->get('chill.main.timeline_builder')
|
||||
->getTimelineHTML('task', array('task' => $task));
|
||||
|
||||
$event = new PrivacyEvent($person, array(
|
||||
'element_class' => SingleTask::class,
|
||||
'element_id' => $task->getId(),
|
||||
'action' => 'show'
|
||||
));
|
||||
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
|
||||
|
||||
return $this->render('ChillTaskBundle:SingleTask:show.html.twig', array(
|
||||
'task' => $task,
|
||||
'timeline' => $timeline
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @Route(
|
||||
* "/{_locale}/task/single-task/{id}/edit",
|
||||
* name="chill_task_single_task_edit"
|
||||
* )
|
||||
*/
|
||||
public function editAction(
|
||||
Request $request,
|
||||
$id,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
/* @var $taskRepository SingleTaskRepository */
|
||||
$taskRepository = $this->get('chill_task.single_task_repository');
|
||||
|
||||
$task = $taskRepository->find($id);
|
||||
|
||||
if (!is_null($task->getPerson() === !null)) {
|
||||
$personId = $task->getPerson()->getId();
|
||||
if ($personId === null) {
|
||||
return new Response("You must provide a person_id", Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$person = $this->getDoctrine()->getManager()
|
||||
->getRepository(Person::class)
|
||||
->find($personId);
|
||||
|
||||
if ($person === null) {
|
||||
throw $this->createNotFoundException("Invalid person id");
|
||||
}
|
||||
}
|
||||
$this->denyAccessUnlessGranted(TaskVoter::UPDATE, $task, 'You are not '
|
||||
. 'allowed to edit this task');
|
||||
|
||||
if (!$task) {
|
||||
throw $this->createNotFoundException('Unable to find Task entity.');
|
||||
}
|
||||
|
||||
$event = (new UIEvent('single-task', $task))
|
||||
->setForm($this->setCreateForm($task, new Role(TaskVoter::UPDATE)))
|
||||
;
|
||||
|
||||
$this->eventDispatcher->dispatch(UIEvent::EDIT_FORM, $event);
|
||||
|
||||
$form = $event->getForm();
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted()) {
|
||||
if ($form->isValid()) {
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->persist($task);
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $translator
|
||||
->trans("The task has been updated"));
|
||||
|
||||
$event = new PrivacyEvent($person, array(
|
||||
'element_class' => SingleTask::class,
|
||||
'element_id' => $task->getId(),
|
||||
'action' => 'update'
|
||||
));
|
||||
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'chill_task_singletask_list',
|
||||
$request->query->get('list_params', [])
|
||||
);
|
||||
|
||||
} else {
|
||||
$this->addFlash('error', $translator->trans("This form contains errors"));
|
||||
}
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(UIEvent::EDIT_PAGE, $event);
|
||||
|
||||
if ($event->hasResponse()) {
|
||||
return $event->getResponse();
|
||||
}
|
||||
|
||||
$event = new PrivacyEvent($person, array(
|
||||
'element_class' => SingleTask::class,
|
||||
'element_id' => $task->getId(),
|
||||
'action' => 'edit'
|
||||
));
|
||||
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
|
||||
|
||||
return $this->render('ChillTaskBundle:SingleTask:edit.html.twig', array(
|
||||
'task' => $task,
|
||||
'form' => $form->createView()
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @Route(
|
||||
* "/{_locale}/task/single-task/{id}/delete",
|
||||
* name="chill_task_single_task_delete"
|
||||
* )
|
||||
*/
|
||||
public function deleteAction(
|
||||
Request $request,
|
||||
$id,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
/* @var $taskRepository SingleTaskRepository */
|
||||
$taskRepository = $this->get('chill_task.single_task_repository');
|
||||
|
||||
$task = $taskRepository->find($id);
|
||||
|
||||
if (!$task) {
|
||||
throw $this->createNotFoundException('Unable to find Task entity.');
|
||||
}
|
||||
|
||||
if (!is_null($task->getPerson() === !null)) {
|
||||
|
||||
$personId = $task->getPerson()->getId();
|
||||
if ($personId === null) {
|
||||
return new Response("You must provide a person_id", Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$person = $this->getDoctrine()->getManager()
|
||||
->getRepository(Person::class)
|
||||
->find($personId);
|
||||
|
||||
if ($person === null) {
|
||||
throw $this->createNotFoundException("Invalid person id");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(TaskVoter::DELETE, $task, 'You are not '
|
||||
. 'allowed to delete this task');
|
||||
|
||||
$form = $this->createDeleteForm($id);
|
||||
|
||||
if ($request->getMethod() === Request::METHOD_DELETE) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$logger = $this->get('chill.main.logger');
|
||||
|
||||
$logger->notice("A task has been removed", array(
|
||||
'by_user' => $this->getUser()->getUsername(),
|
||||
'task_id' => $task->getId(),
|
||||
'description' => $task->getDescription(),
|
||||
'assignee' => $task->getAssignee(),
|
||||
'scope_id' => $task->getScope()->getId(),
|
||||
//'start_date' => $task->getStartDate()->format('Y-m-d'),
|
||||
//'end_date' => $task->getEndDate()->format('Y-m-d'),
|
||||
//'warning_interval' => $task->getWarningInterval()->format('Y-m-d')
|
||||
));
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->remove($task);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $translator
|
||||
->trans("The task has been successfully removed."));
|
||||
|
||||
return $this->redirect($this->generateUrl(
|
||||
'chill_task_singletask_list',
|
||||
$request->query->get('list_params', [
|
||||
'person_id' => $person->getId()
|
||||
])));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $this->render('ChillTaskBundle:SingleTask:confirm_delete.html.twig', array(
|
||||
'task' => $task,
|
||||
'delete_form' => $form->createView()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param SingleTask $task
|
||||
* @param Role $role
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function setCreateForm(SingleTask $task, Role $role)
|
||||
{
|
||||
$form = $this->createForm(SingleTaskType::class, $task, [
|
||||
'center' => $task->getCenter(),
|
||||
'role' => $role
|
||||
]);
|
||||
|
||||
$form->add('submit', SubmitType::class);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return Response
|
||||
* @Route(
|
||||
* "/{_locale}/task/single-task/list/my",
|
||||
* name="chill_task_single_my_tasks"
|
||||
* )
|
||||
*/
|
||||
public function myTasksAction(TranslatorInterface $translator)
|
||||
{
|
||||
return $this->redirectToRoute('chill_task_singletask_list', [
|
||||
'user_id' => $this->getUser()->getId(),
|
||||
'hide_form' => true,
|
||||
'title' => $translator->trans('My tasks')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Arguments:
|
||||
* - user_id
|
||||
* - scope_id
|
||||
* - person_id
|
||||
* - hide_form (hide the form to filter the tasks)
|
||||
* - status: date state, amongst SingleTaskRepository::DATE_STATUSES, or 'closed'
|
||||
*
|
||||
* @Route(
|
||||
* "/{_locale}/task/singletask/list",
|
||||
* name="chill_task_singletask_list"
|
||||
* )
|
||||
*/
|
||||
public function listAction(
|
||||
Request $request,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
SingleTaskRepository $taskRepository,
|
||||
PersonRepository $personRepository,
|
||||
CenterRepository $centerRepository,
|
||||
FormFactoryInterface $formFactory
|
||||
) {
|
||||
/* @var $viewParams array The parameters for the view */
|
||||
/* @var $params array The parameters for the query */
|
||||
|
||||
$viewParams['person'] = null;
|
||||
$params['person'] = null;
|
||||
$viewParams['user'] = null;
|
||||
$params['user'] = null;
|
||||
$viewParams['center'] = null;
|
||||
$params['types'] = null;
|
||||
|
||||
// Get parameters from url
|
||||
if (!empty($request->query->get('person_id', NULL))) {
|
||||
$personId = $request->query->getInt('person_id');
|
||||
$person = $personRepository->find($personId);
|
||||
|
||||
if ($person === null) {
|
||||
throw $this->createNotFoundException("This person ' $personId ' does not exist.");
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(PersonVoter::SEE, $person);
|
||||
|
||||
$viewParams['person'] = $person;
|
||||
$params['person'] = $person;
|
||||
} elseif (!empty($request->query->get('center_id', NULL))) {
|
||||
$center = $centerRepository->find($request->query->getInt('center_id'));
|
||||
|
||||
if ($center === null) {
|
||||
throw $this->createNotFoundException('center not found');
|
||||
}
|
||||
|
||||
$params['center'] = $center;
|
||||
}
|
||||
|
||||
if(!empty($request->query->get('types', []))) {
|
||||
$types = $request->query->get('types', []);
|
||||
if (count($types) > 0) {
|
||||
$params['types'] = $types;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($request->query->get('user_id', null))) {
|
||||
if ($request->query->get('user_id') === '_unassigned') {
|
||||
$params['unassigned'] = true;
|
||||
} else {
|
||||
$userId = $request->query->getInt('user_id', null);
|
||||
$user = $this->getDoctrine()->getManager()
|
||||
->getRepository('ChillMainBundle:User')
|
||||
->find($userId);
|
||||
|
||||
if ($user === null) {
|
||||
throw $this->createNotFoundException("This user ' $userId ' does not exist.");
|
||||
}
|
||||
|
||||
$viewParams['user'] = $user;
|
||||
$params['user'] = $user;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($request->query->get('scope_id'))) {
|
||||
|
||||
$scopeId = $request->query->getInt('scope_id', null);
|
||||
$scope = $this->getDoctrine()->getManager()
|
||||
->getRepository('ChillMainBundle:Scope')
|
||||
->find($scopeId);
|
||||
|
||||
if ($scope === null) {
|
||||
throw $this->createNotFoundException("This scope' $scopeId 'does not exist.");
|
||||
}
|
||||
|
||||
$viewParams['scope'] = $scope;
|
||||
$params['scope'] = $scope;
|
||||
}
|
||||
|
||||
// collect parameters for filter
|
||||
$possibleStatuses = \array_merge(SingleTaskRepository::DATE_STATUSES, [ 'closed' ]);
|
||||
$statuses = $request->query->get('status', $possibleStatuses);
|
||||
|
||||
// check for invalid statuses
|
||||
$diff = \array_diff($statuses, $possibleStatuses);
|
||||
if (count($diff) > 0) {
|
||||
return new Response(
|
||||
'date_status not allowed: '. \implode(', ', $diff),
|
||||
Response::HTTP_BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
$viewParams['isSingleStatus'] = $singleStatus = count($statuses) === 1;
|
||||
|
||||
$tasks_count = 0;
|
||||
|
||||
foreach($statuses as $status) {
|
||||
if($request->query->has('status')
|
||||
&& FALSE === \in_array($status, $statuses)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// different query if regarding to date or 'closed'
|
||||
if (in_array($status, SingleTaskRepository::DATE_STATUSES)) {
|
||||
$params['date_status'] = $status;
|
||||
$params['is_closed'] = false;
|
||||
} else {
|
||||
$params['date_status'] = null;
|
||||
$params['is_closed'] = true;
|
||||
}
|
||||
|
||||
$count = $taskRepository
|
||||
->countByParameters($params, $this->getUser())
|
||||
;
|
||||
$paginator = $paginatorFactory->create($count);
|
||||
|
||||
$viewParams['single_task_'.$status.'_count'] = $count;
|
||||
$viewParams['single_task_'.$status.'_paginator'] = $paginator;
|
||||
$viewParams['single_task_'.$status.'_tasks'] = $taskRepository
|
||||
->findByParameters($params, $this->getUser(),
|
||||
$singleStatus ? $paginator->getCurrentPage()->getFirstItemNumber() : 0,
|
||||
$singleStatus ? $paginator->getItemsPerPage() : 10)
|
||||
;
|
||||
|
||||
$tasks_count = $tasks_count + $count;
|
||||
}
|
||||
|
||||
// total number of tasks
|
||||
$viewParams['tasks_count'] = $tasks_count;
|
||||
|
||||
if ($viewParams['person'] !== null){
|
||||
$viewParams['layout'] = 'ChillPersonBundle::layout.html.twig';
|
||||
} else {
|
||||
$viewParams['layout'] = 'ChillMainBundle::layout.html.twig';
|
||||
}
|
||||
|
||||
// Form for filtering tasks
|
||||
$form = $formFactory->createNamed(null, SingleTaskListType::class, null, [
|
||||
'person' => $viewParams['person'],
|
||||
'method' => Request::METHOD_GET,
|
||||
'csrf_protection' => false,
|
||||
'add_type' => true
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if (isset($person)) {
|
||||
$event = new PrivacyEvent($person, array(
|
||||
'element_class' => SingleTask::class,
|
||||
'action' => 'list'
|
||||
));
|
||||
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
|
||||
}
|
||||
|
||||
return $this->render('ChillTaskBundle:SingleTask:index.html.twig',
|
||||
\array_merge($viewParams, [ 'form' => $form->createView() ]));
|
||||
}
|
||||
|
||||
|
||||
protected function getPersonParam(Request $request, EntityManagerInterface $em)
|
||||
{
|
||||
$person = $em->getRepository(Person::class)
|
||||
->find($request->query->getInt('person_id'))
|
||||
;
|
||||
|
||||
if (NULL === $person) {
|
||||
throw $this->createNotFoundException('person not found');
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(PersonVoter::SEE, $person, "You are "
|
||||
. "not allowed to see this person");
|
||||
|
||||
return $person;
|
||||
}
|
||||
|
||||
protected function getUserParam(Request $request, EntityManagerInterface $em)
|
||||
{
|
||||
$user = $em->getRepository(User::class)
|
||||
->find($request->query->getInt('user_id'))
|
||||
;
|
||||
|
||||
if (NULL === $user) {
|
||||
throw $this->createNotFoundException('user not found');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form to delete a Task entity by id.
|
||||
*
|
||||
* @param mixed $id The entity id
|
||||
*
|
||||
* @return \Symfony\Component\Form\Form The form
|
||||
*/
|
||||
private function createDeleteForm($id)
|
||||
{
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl(
|
||||
'chill_task_single_task_delete',
|
||||
array('id' => $id)))
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, array('label' => 'Delete'))
|
||||
->getForm()
|
||||
;
|
||||
}
|
||||
|
||||
}
|
142
src/Bundle/ChillTask/Controller/TaskController.php
Normal file
142
src/Bundle/ChillTask/Controller/TaskController.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Chill\TaskBundle\Repository\SingleTaskRepository;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Chill\TaskBundle\Event\UI\UIEvent;
|
||||
use Chill\TaskBundle\Entity\AbstractTask;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
|
||||
class TaskController extends Controller
|
||||
{
|
||||
/**
|
||||
* Apply a transition to a task
|
||||
*
|
||||
* @Route(
|
||||
* "/{_locale}/task/transition/{kind}/{taskId}/{transition}",
|
||||
* name="chill_task_task_transition"
|
||||
* )
|
||||
*
|
||||
* @param string $kind
|
||||
* @param int $taskId
|
||||
* @param string $transition
|
||||
* @param SingleTaskRepository $singleTaskRepository
|
||||
* @param Registry $registry
|
||||
* @param EntityManagerInterface $em
|
||||
* @param Request $request
|
||||
* @param TranslatorInterface $translator
|
||||
* @return Response
|
||||
*/
|
||||
public function applyTransitionAction(
|
||||
$kind,
|
||||
$taskId,
|
||||
$transition,
|
||||
SingleTaskRepository $singleTaskRepository,
|
||||
Registry $registry,
|
||||
EntityManagerInterface $em,
|
||||
Request $request,
|
||||
TranslatorInterface $translator,
|
||||
EventDispatcherInterface $eventDispatcher
|
||||
) {
|
||||
switch ($kind) {
|
||||
case 'single-task':
|
||||
$task = $singleTaskRepository
|
||||
->find($taskId)
|
||||
;
|
||||
$defaultReturnPath = $this->generateUrl(
|
||||
'chill_task_single_task_show',
|
||||
[
|
||||
'id' => $task->getId(),
|
||||
'list_params' => $request->query->get('list_params', [])
|
||||
]);
|
||||
$defaultTemplate = '@ChillTask/SingleTask/transition.html.twig';
|
||||
break;
|
||||
default:
|
||||
return new Response("The type '$kind' is not implemented",
|
||||
Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (NULL === $task) {
|
||||
throw $this->createNotFoundException("task with id '$taskId' and type "
|
||||
. "'$type' does not exists");
|
||||
}
|
||||
|
||||
$workflow = $registry->get($task);
|
||||
|
||||
if (!$workflow->can($task, $transition)) {
|
||||
throw $this->createAccessDeniedException('You are not allowed to apply this transition');
|
||||
}
|
||||
$transitionInstance = \array_values( // array_values needed to reset keys (array_filter preserves keys)
|
||||
\array_filter(
|
||||
$workflow->getEnabledTransitions($task),
|
||||
function(Transition $t) use ($transition) {
|
||||
return $t->getName() === $transition;
|
||||
}
|
||||
))[0];
|
||||
|
||||
$form = $this->createTransitionForm($task);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
|
||||
if ($workflow->can($task, $transition)) {
|
||||
$workflow->apply($task, $transition);
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $translator->trans('The transition is successfully applied'));
|
||||
|
||||
} else {
|
||||
$this->addFlash('error', $translator->trans('The transition could not be applied'));
|
||||
}
|
||||
|
||||
return $this->redirect($defaultReturnPath);
|
||||
} else {
|
||||
$event = (new UIEvent($kind, $task))
|
||||
->setForm($form)
|
||||
->setTransition($transitionInstance)
|
||||
;
|
||||
|
||||
$eventDispatcher->dispatch(UIEvent::SHOW_TRANSITION_PAGE, $event);
|
||||
|
||||
if ($event->hasResponse()) {
|
||||
return $event->getResponse();
|
||||
} else {
|
||||
// we simply check that the user can see the task. Other ACL checks
|
||||
// should be performed using `guard` events.
|
||||
$this->denyAccessUnlessGranted(TaskVoter::SHOW, $task);
|
||||
|
||||
return $this->render($defaultTemplate, [
|
||||
'task' => $task,
|
||||
'form' => $form->createView(),
|
||||
'transition' => $transitionInstance
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param \Chill\TaskBundle\Controller\AbstractTask $task
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function createTransitionForm(AbstractTask $task)
|
||||
{
|
||||
$builder = $this->createFormBuilder($task);
|
||||
$builder->add('submit', SubmitType::class);
|
||||
|
||||
return $builder->getForm();
|
||||
}
|
||||
}
|
91
src/Bundle/ChillTask/DataFixtures/ORM/LoadTaskACL.php
Normal file
91
src/Bundle/ChillTask/DataFixtures/ORM/LoadTaskACL.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2015 Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Chill\TaskBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup;
|
||||
use Chill\MainBundle\Entity\RoleScope;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadScopes;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
|
||||
/**
|
||||
* Add a role UPDATE & CREATE for all groups except administrative,
|
||||
* and a role SEE for administrative
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class LoadTaskACL extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function getOrder()
|
||||
{
|
||||
return 16000;
|
||||
}
|
||||
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) {
|
||||
$permissionsGroup = $this->getReference($permissionsGroupRef);
|
||||
foreach (LoadScopes::$references as $scopeRef){
|
||||
$scope = $this->getReference($scopeRef);
|
||||
//create permission group
|
||||
switch ($permissionsGroup->getName()) {
|
||||
case 'social':
|
||||
if ($scope->getName()['en'] === 'administrative') {
|
||||
break 2; // we do not want any power on administrative
|
||||
}
|
||||
break;
|
||||
case 'administrative':
|
||||
case 'direction':
|
||||
if (in_array($scope->getName()['en'], array('administrative', 'social'))) {
|
||||
break 2; // we do not want any power on social or administrative
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
printf("Adding CHILL_TASK_TASK_UPDATE & CHILL_TASK_TASK_CREATE & Chill_TASK_TASK_DELETE permissions to %s "
|
||||
. "permission group, scope '%s' \n",
|
||||
$permissionsGroup->getName(), $scope->getName()['en']);
|
||||
$roleScopeUpdate = (new RoleScope())
|
||||
->setRole(TaskVoter::UPDATE)
|
||||
->setScope($scope);
|
||||
$permissionsGroup->addRoleScope($roleScopeUpdate);
|
||||
$roleScopeCreate = (new RoleScope())
|
||||
->setRole(TaskVoter::CREATE)
|
||||
->setScope($scope);
|
||||
$permissionsGroup->addRoleScope($roleScopeCreate);
|
||||
$roleScopeDelete = (new RoleScope())
|
||||
->setRole(TaskVoter::DELETE)
|
||||
->setScope($scope);
|
||||
$permissionsGroup->addRoleScope($roleScopeDelete);
|
||||
|
||||
$manager->persist($roleScopeUpdate);
|
||||
$manager->persist($roleScopeCreate);
|
||||
$manager->persist($roleScopeDelete);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
}
|
103
src/Bundle/ChillTask/DependencyInjection/ChillTaskExtension.php
Normal file
103
src/Bundle/ChillTask/DependencyInjection/ChillTaskExtension.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||
use Chill\TaskBundle\Workflow\TaskWorkflowManager;
|
||||
|
||||
/**
|
||||
* This is the class that loads and manages your bundle configuration.
|
||||
*
|
||||
* @link http://symfony.com/doc/current/cookbook/bundles/extension.html
|
||||
*/
|
||||
class ChillTaskExtension extends Extension implements PrependExtensionInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container)
|
||||
{
|
||||
$configuration = new Configuration();
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
|
||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
|
||||
$loader->load('services/controller.yml');
|
||||
$loader->load('services/security.yml');
|
||||
$loader->load('services/repositories.yml');
|
||||
$loader->load('services/workflow.yml');
|
||||
$loader->load('services/templating.yml');
|
||||
$loader->load('services/menu.yml');
|
||||
$loader->load('services/event.yml');
|
||||
$loader->load('services/timeline.yml');
|
||||
$loader->load('services/fixtures.yml');
|
||||
$loader->load('services/form.yml');
|
||||
}
|
||||
|
||||
public function prepend(ContainerBuilder $container)
|
||||
{
|
||||
$this->prependAuthorization($container);
|
||||
$this->prependRoute($container);
|
||||
$this->prependWorkflows($container);
|
||||
}
|
||||
|
||||
protected function prependRoute(ContainerBuilder $container)
|
||||
{
|
||||
//declare routes for task bundle
|
||||
$container->prependExtensionConfig('chill_main', array(
|
||||
'routing' => array(
|
||||
'resources' => array(
|
||||
'@ChillTaskBundle/Resources/config/routing.yml'
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
protected function prependAuthorization(ContainerBuilder $container)
|
||||
{
|
||||
$container->prependExtensionConfig('security', array(
|
||||
'role_hierarchy' => array(
|
||||
TaskVoter::UPDATE => [TaskVoter::SHOW],
|
||||
TaskVoter::CREATE => [TaskVoter::SHOW]
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
protected function prependWorkflows(ContainerBuilder $container)
|
||||
{
|
||||
$container->prependExtensionConfig('framework', [
|
||||
'workflows' => [
|
||||
'task_default' => [
|
||||
'marking_store' => [
|
||||
'type' => 'multiple_state',
|
||||
'arguments' => [
|
||||
'currentStates'
|
||||
],
|
||||
],
|
||||
'type' => 'state_machine',
|
||||
'support_strategy' => TaskWorkflowManager::class,
|
||||
'places' => [ 'new', 'in_progress', 'closed', 'canceled'],
|
||||
'initial_place' => 'new',
|
||||
'transitions' => [
|
||||
'start' => [
|
||||
'from' => 'new',
|
||||
'to' => 'in_progress'
|
||||
],
|
||||
'close' => [
|
||||
'from' => ['new', 'in_progress'],
|
||||
'to' => 'closed'
|
||||
],
|
||||
'cancel' => [
|
||||
'from' => ['new', 'in_progress'],
|
||||
'to' => 'canceled'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Chill\TaskBundle\Workflow\TaskWorkflowManager;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Chill\TaskBundle\Templating\UI\CountNotificationTask;
|
||||
use Chill\TaskBundle\Event\Lifecycle\TaskLifecycleEvent;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class TaskWorkflowDefinitionCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasDefinition(TaskWorkflowManager::class)) {
|
||||
throw new \LogicException("The service ".TaskWorkflowManager::class." is "
|
||||
. "not registered");
|
||||
}
|
||||
|
||||
$workflowManagerDefinition = $container->getDefinition(TaskWorkflowManager::class);
|
||||
$counterDefinition = $container->getDefinition(CountNotificationTask::class);
|
||||
$lifecycleDefinition = $container->getDefinition(TaskLifecycleEvent::class);
|
||||
|
||||
foreach ($container->findTaggedServiceIds('chill_task.workflow_definition') as $id => $tags) {
|
||||
// registering the definition to manager
|
||||
$workflowManagerDefinition
|
||||
->addMethodCall('addDefinition', [new Reference($id)]);
|
||||
// adding a listener for currentStatus changes
|
||||
$definition = $container->getDefinition($id);
|
||||
$workflowManagerDefinition
|
||||
->addTag('kernel.event_listener', [
|
||||
'event' => sprintf('workflow.%s.entered', $definition->getClass()::getAssociatedWorkflowName()),
|
||||
'method' => 'onTaskStateEntered',
|
||||
'priority' => -255
|
||||
]);
|
||||
$counterDefinition
|
||||
->addTag('kernel.event_listener', [
|
||||
'event' => sprintf('workflow.%s.entered', $definition->getClass()::getAssociatedWorkflowName()),
|
||||
'method' => 'resetCacheOnNewStates',
|
||||
'priority' => 0
|
||||
]);
|
||||
$lifecycleDefinition
|
||||
->addTag('kernel.event_listener', [
|
||||
'event' => sprintf('workflow.%s.transition', $definition->getClass()::getAssociatedWorkflowName()),
|
||||
'method' => 'onTransition',
|
||||
'priority' => 0
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
29
src/Bundle/ChillTask/DependencyInjection/Configuration.php
Normal file
29
src/Bundle/ChillTask/DependencyInjection/Configuration.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
|
||||
/**
|
||||
* This is the class that validates and merges configuration from your app/config files.
|
||||
*
|
||||
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/configuration.html}
|
||||
*/
|
||||
class Configuration implements ConfigurationInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfigTreeBuilder()
|
||||
{
|
||||
$treeBuilder = new TreeBuilder();
|
||||
$rootNode = $treeBuilder->root('chill_task');
|
||||
|
||||
// Here you should define the parameters that are allowed to
|
||||
// configure your bundle. See the documentation linked above for
|
||||
// more information on that topic.
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
}
|
260
src/Bundle/ChillTask/Entity/AbstractTask.php
Normal file
260
src/Bundle/ChillTask/Entity/AbstractTask.php
Normal file
@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\HasScopeInterface;
|
||||
use Chill\MainBundle\Entity\HasCenterInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistency;
|
||||
|
||||
/**
|
||||
* AbstractTask
|
||||
*
|
||||
* @ORM\MappedSuperclass()
|
||||
* @UserCircleConsistency(
|
||||
* "CHILL_TASK_TASK_SHOW",
|
||||
* getUserFunction="getAssignee"
|
||||
* )
|
||||
*/
|
||||
abstract class AbstractTask implements HasScopeInterface, HasCenterInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="type", type="string", length=255)
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var json
|
||||
*
|
||||
* @ORM\Column(name="current_states", type="json")
|
||||
*/
|
||||
private $currentStates = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="title", type="text")
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $title = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="description", type="text")
|
||||
*/
|
||||
private $description = '';
|
||||
|
||||
/**
|
||||
*
|
||||
* @var User
|
||||
* @ORM\ManyToOne(
|
||||
* targetEntity="\Chill\MainBundle\Entity\User"
|
||||
* )
|
||||
*/
|
||||
private $assignee;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Person
|
||||
* @ORM\ManyToOne(
|
||||
* targetEntity="\Chill\PersonBundle\Entity\Person"
|
||||
* )
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $person;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Scope
|
||||
* @ORM\ManyToOne(
|
||||
* targetEntity="\Chill\MainBundle\Entity\Scope"
|
||||
* )
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $circle;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
* @ORM\Column(name="closed", type="boolean", options={ "default"=false })
|
||||
*/
|
||||
private $closed = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return AbstractTask
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = (string) $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set currentStates
|
||||
*
|
||||
* The current states are sorted in a single array, non associative.
|
||||
*
|
||||
* @param $currentStates
|
||||
*
|
||||
* @return AbstractTask
|
||||
*/
|
||||
public function setCurrentStates($currentStates)
|
||||
{
|
||||
$this->currentStates = \array_keys($currentStates);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currentStates
|
||||
*
|
||||
* The states are returned as required by marking store format.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCurrentStates()
|
||||
{
|
||||
return \array_fill_keys($this->currentStates, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set title
|
||||
*
|
||||
* @param string $title
|
||||
*
|
||||
* @return AbstractTask
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
$this->title = (string) $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description
|
||||
*
|
||||
* @param string $description
|
||||
*
|
||||
* @return AbstractTask
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->description = (string) $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getAssignee(): ?User
|
||||
{
|
||||
return $this->assignee;
|
||||
}
|
||||
|
||||
public function getPerson(): ?Person
|
||||
{
|
||||
return $this->person;
|
||||
}
|
||||
|
||||
public function getCircle(): ?Scope
|
||||
{
|
||||
return $this->circle;
|
||||
}
|
||||
|
||||
public function setAssignee(User $assignee = null)
|
||||
{
|
||||
$this->assignee = $assignee;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPerson(Person $person)
|
||||
{
|
||||
$this->person = $person;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCircle(Scope $circle)
|
||||
{
|
||||
$this->circle = $circle;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCenter(): ?\Chill\MainBundle\Entity\Center
|
||||
{
|
||||
if ($this->getPerson() instanceof Person) {
|
||||
return $this->getPerson()->getCenter();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
public function getScope(): ?\Chill\MainBundle\Entity\Scope
|
||||
{
|
||||
return $this->getCircle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isClosed(): bool
|
||||
{
|
||||
return $this->closed;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param bool $closed
|
||||
*/
|
||||
public function setClosed(bool $closed)
|
||||
{
|
||||
$this->closed = $closed;
|
||||
}
|
||||
}
|
||||
|
210
src/Bundle/ChillTask/Entity/RecurringTask.php
Normal file
210
src/Bundle/ChillTask/Entity/RecurringTask.php
Normal file
@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
/**
|
||||
* RecurringTask
|
||||
*
|
||||
* @ORM\Table(name="chill_task.recurring_task")
|
||||
* @ORM\Entity(repositoryClass="Chill\TaskBundle\Repository\RecurringTaskRepository")
|
||||
*/
|
||||
class RecurringTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(name="first_occurence_end_date", type="date")
|
||||
*/
|
||||
private $firstOccurenceEndDate;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(name="last_occurence_end_date", type="date")
|
||||
*/
|
||||
private $lastOccurenceEndDate;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="occurence_frequency", type="string", length=255)
|
||||
*/
|
||||
private $occurenceFrequency;
|
||||
|
||||
/**
|
||||
* @var dateinterval
|
||||
*
|
||||
* @ORM\Column(name="occurence_start_date", type="dateinterval")
|
||||
*/
|
||||
private $occurenceStartDate;
|
||||
|
||||
/**
|
||||
* @var dateinterval
|
||||
*
|
||||
* @ORM\Column(name="occurence_warning_interval", type="dateinterval", nullable=true)
|
||||
*/
|
||||
private $occurenceWarningInterval;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Collection
|
||||
*
|
||||
* @ORM\OneToMany(
|
||||
* targetEntity="SingleTask",
|
||||
* mappedBy="recurringTask"
|
||||
* )
|
||||
*/
|
||||
private $singleTasks;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->singleTasks = new ArrayCollection();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get id
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set firstOccurenceEndDate
|
||||
*
|
||||
* @param \DateTime $firstOccurenceEndDate
|
||||
*
|
||||
* @return RecurringTask
|
||||
*/
|
||||
public function setFirstOccurenceEndDate($firstOccurenceEndDate)
|
||||
{
|
||||
$this->firstOccurenceEndDate = $firstOccurenceEndDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get firstOccurenceEndDate
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getFirstOccurenceEndDate()
|
||||
{
|
||||
return $this->firstOccurenceEndDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lastOccurenceEndDate
|
||||
*
|
||||
* @param \DateTime $lastOccurenceEndDate
|
||||
*
|
||||
* @return RecurringTask
|
||||
*/
|
||||
public function setLastOccurenceEndDate($lastOccurenceEndDate)
|
||||
{
|
||||
$this->lastOccurenceEndDate = $lastOccurenceEndDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lastOccurenceEndDate
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getLastOccurenceEndDate()
|
||||
{
|
||||
return $this->lastOccurenceEndDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set occurenceFrequency
|
||||
*
|
||||
* @param string $occurenceFrequency
|
||||
*
|
||||
* @return RecurringTask
|
||||
*/
|
||||
public function setOccurenceFrequency($occurenceFrequency)
|
||||
{
|
||||
$this->occurenceFrequency = $occurenceFrequency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get occurenceFrequency
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOccurenceFrequency()
|
||||
{
|
||||
return $this->occurenceFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set occurenceStartDate
|
||||
*
|
||||
* @param dateinterval $occurenceStartDate
|
||||
*
|
||||
* @return RecurringTask
|
||||
*/
|
||||
public function setOccurenceStartDate($occurenceStartDate)
|
||||
{
|
||||
$this->occurenceStartDate = $occurenceStartDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get occurenceStartDate
|
||||
*
|
||||
* @return dateinterval
|
||||
*/
|
||||
public function getOccurenceStartDate()
|
||||
{
|
||||
return $this->occurenceStartDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set occurenceWarningInterval
|
||||
*
|
||||
* @param dateinterval $occurenceWarningInterval
|
||||
*
|
||||
* @return RecurringTask
|
||||
*/
|
||||
public function setOccurenceWarningInterval($occurenceWarningInterval)
|
||||
{
|
||||
$this->occurenceWarningInterval = $occurenceWarningInterval;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get occurenceWarningInterval
|
||||
*
|
||||
* @return dateinterval
|
||||
*/
|
||||
public function getOccurenceWarningInterval()
|
||||
{
|
||||
return $this->occurenceWarningInterval;
|
||||
}
|
||||
}
|
||||
|
234
src/Bundle/ChillTask/Entity/SingleTask.php
Normal file
234
src/Bundle/ChillTask/Entity/SingleTask.php
Normal file
@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
/**
|
||||
* SingleTask
|
||||
*
|
||||
* @ORM\Table(
|
||||
* "chill_task.single_task",
|
||||
* indexes={
|
||||
* @ORM\Index(
|
||||
* name="by_type",
|
||||
* columns={ "type" }
|
||||
* ),
|
||||
* @ORM\Index(
|
||||
* name="by_current_state",
|
||||
* columns={ "current_states" }
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="Chill\TaskBundle\Repository\SingleTaskRepository")
|
||||
*/
|
||||
class SingleTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(name="start_date", type="date", nullable=true)
|
||||
* @Assert\Date()
|
||||
*
|
||||
* @Assert\Expression(
|
||||
* "value === null or this.getEndDate() === null or value < this.getEndDate()",
|
||||
* message="The start date must be before the end date"
|
||||
* )
|
||||
*
|
||||
* @Assert\Expression(
|
||||
* "value === null or this.getWarningDate() === null or this.getWarningDate() > this.getStartDate()",
|
||||
* message="The start date must be before warning date"
|
||||
* )
|
||||
*/
|
||||
private $startDate;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(name="end_date", type="date", nullable=true)
|
||||
* @Assert\Date()
|
||||
*/
|
||||
private $endDate;
|
||||
|
||||
/**
|
||||
* @var \DateInterval
|
||||
* and this.getEndDate() === null
|
||||
*
|
||||
* @ORM\Column(name="warning_interval", type="dateinterval", nullable=true)
|
||||
*
|
||||
* @Assert\Expression(
|
||||
* "!(value != null and this.getEndDate() == null)",
|
||||
* message="An end date is required if a warning interval is set"
|
||||
* )
|
||||
*
|
||||
*
|
||||
*/
|
||||
private $warningInterval;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var RecurringTask
|
||||
* @ORM\ManyToOne(
|
||||
* targetEntity="RecurringTask",
|
||||
* inversedBy="singleTasks"
|
||||
* )
|
||||
*/
|
||||
private $recurringTask;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var \Doctrine\Common\Collections\Collection
|
||||
* @ORM\OneToMany(
|
||||
* targetEntity="\Chill\TaskBundle\Entity\Task\SingleTaskPlaceEvent",
|
||||
* mappedBy="task",
|
||||
* cascade={ "remove" }
|
||||
* )
|
||||
*/
|
||||
private $taskPlaceEvents;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->taskPlaceEvents = $events = new \Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get id
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set startDate
|
||||
*
|
||||
* @param \DateTime $startDate
|
||||
*
|
||||
* @return SingleTask
|
||||
*/
|
||||
public function setStartDate($startDate)
|
||||
{
|
||||
$this->startDate = $startDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get startDate
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getStartDate()
|
||||
{
|
||||
return $this->startDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set endDate
|
||||
*
|
||||
* @param \DateTime $endDate
|
||||
*
|
||||
* @return SingleTask
|
||||
*/
|
||||
public function setEndDate($endDate)
|
||||
{
|
||||
$this->endDate = $endDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get endDate
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getEndDate()
|
||||
{
|
||||
return $this->endDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set warningInterval
|
||||
*
|
||||
* @param string $warningInterval
|
||||
*
|
||||
* @return SingleTask
|
||||
*/
|
||||
public function setWarningInterval($warningInterval)
|
||||
{
|
||||
$this->warningInterval = $warningInterval;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get warningInterval
|
||||
*
|
||||
* @return \DateInterval
|
||||
*/
|
||||
public function getWarningInterval()
|
||||
{
|
||||
return $this->warningInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Warning date, computed from the difference between the
|
||||
* end date and the warning interval
|
||||
*
|
||||
* Return null if warningDate or endDate is null
|
||||
*
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function getWarningDate()
|
||||
{
|
||||
if ($this->getWarningInterval() === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->getEndDate() === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \DateTimeImmutable::createFromMutable($this->getEndDate())
|
||||
->sub($this->getWarningInterval());
|
||||
}
|
||||
|
||||
function getRecurringTask(): RecurringTask
|
||||
{
|
||||
return $this->recurringTask;
|
||||
}
|
||||
|
||||
function setRecurringTask(RecurringTask $recurringTask)
|
||||
{
|
||||
$this->recurringTask = $recurringTask;
|
||||
}
|
||||
|
||||
public function getTaskPlaceEvents(): Collection
|
||||
{
|
||||
return $this->taskPlaceEvents;
|
||||
}
|
||||
|
||||
public function setTaskPlaceEvents(Collection $taskPlaceEvents)
|
||||
{
|
||||
$this->taskPlaceEvents = $taskPlaceEvents;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
154
src/Bundle/ChillTask/Entity/Task/AbstractTaskPlaceEvent.php
Normal file
154
src/Bundle/ChillTask/Entity/Task/AbstractTaskPlaceEvent.php
Normal file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Entity\Task;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
/**
|
||||
* AbstractTaskPlaceEvent
|
||||
*
|
||||
* @ORM\MappedSuperclass()
|
||||
*/
|
||||
class AbstractTaskPlaceEvent
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var datetime_immutable
|
||||
*
|
||||
* @ORM\Column(name="datetime", type="datetime_immutable")
|
||||
*/
|
||||
private $datetime;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="transition", type="string", length=255)
|
||||
*/
|
||||
private $transition = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="data", type="json")
|
||||
*/
|
||||
private $data = [];
|
||||
|
||||
/**
|
||||
*
|
||||
* @var User
|
||||
* @ORM\ManyToOne(
|
||||
* targetEntity="\Chill\MainBundle\Entity\User"
|
||||
* )
|
||||
*/
|
||||
private $author;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->datetime = new \DateTimeImmutable('now');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set datetime.
|
||||
*
|
||||
* @param datetime_immutable $datetime
|
||||
*
|
||||
* @return AbstractTaskPlaceEvent
|
||||
*/
|
||||
public function setDatetime($datetime)
|
||||
{
|
||||
$this->datetime = $datetime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get datetime.
|
||||
*
|
||||
* @return datetime_immutable
|
||||
*/
|
||||
public function getDatetime()
|
||||
{
|
||||
return $this->datetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set transition.
|
||||
*
|
||||
* @param string $transition
|
||||
*
|
||||
* @return AbstractTaskPlaceEvent
|
||||
*/
|
||||
public function setTransition($transition)
|
||||
{
|
||||
$this->transition = $transition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transition.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTransition()
|
||||
{
|
||||
return $this->transition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data.
|
||||
*
|
||||
* @param string $data
|
||||
*
|
||||
* @return AbstractTaskPlaceEvent
|
||||
*/
|
||||
public function setData($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function getAuthor(): User
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function setAuthor(User $author)
|
||||
{
|
||||
$this->author = $author;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
}
|
67
src/Bundle/ChillTask/Entity/Task/SingleTaskPlaceEvent.php
Normal file
67
src/Bundle/ChillTask/Entity/Task/SingleTaskPlaceEvent.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Entity\Task;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @ORM\Table(
|
||||
* "chill_task.single_task_place_event",
|
||||
* indexes={
|
||||
* @ORM\Index(
|
||||
* name="transition_task_date",
|
||||
* columns={"task_id", "transition", "datetime"}
|
||||
* ),
|
||||
* @ORM\Index(
|
||||
* name="transition_task",
|
||||
* columns={"task_id", "transition"}
|
||||
* )
|
||||
* })
|
||||
* @ORM\Entity()
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class SingleTaskPlaceEvent extends AbstractTaskPlaceEvent
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var SingleTask
|
||||
* @ORM\ManyToOne(
|
||||
* targetEntity="\Chill\TaskBundle\Entity\SingleTask",
|
||||
* inversedBy="taskPlaceEvents"
|
||||
* )
|
||||
*/
|
||||
protected $task;
|
||||
|
||||
public function getTask(): SingleTask
|
||||
{
|
||||
return $this->task;
|
||||
}
|
||||
|
||||
public function setTask(SingleTask $task)
|
||||
{
|
||||
$this->task = $task;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
}
|
104
src/Bundle/ChillTask/Event/Lifecycle/TaskLifecycleEvent.php
Normal file
104
src/Bundle/ChillTask/Event/Lifecycle/TaskLifecycleEvent.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Event\Lifecycle;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Chill\TaskBundle\Event\TaskEvent;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Chill\TaskBundle\Entity\Task\SingleTaskPlaceEvent;
|
||||
use Symfony\Component\Workflow\Event\Event as WorkflowEvent;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class TaskLifecycleEvent implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $tokenStorage;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(
|
||||
TokenStorageInterface $tokenStorage,
|
||||
EntityManagerInterface $em
|
||||
) {
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
TaskEvent::PERSIST => [
|
||||
'onTaskPersist'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function onTaskPersist(TaskEvent $e)
|
||||
{
|
||||
$task = $e->getTask();
|
||||
$user = $this->tokenStorage->getToken()->getUser();
|
||||
|
||||
$event = (new SingleTaskPlaceEvent())
|
||||
->setTask($task)
|
||||
->setAuthor($user)
|
||||
->setTransition('_creation')
|
||||
->setData([
|
||||
'new_states' => $task->getCurrentStates()
|
||||
])
|
||||
;
|
||||
|
||||
$task->getTaskPlaceEvents()->add($event);
|
||||
|
||||
$this->em->persist($event);
|
||||
}
|
||||
|
||||
public function onTransition(WorkflowEvent $e)
|
||||
{
|
||||
$task = $e->getSubject();
|
||||
$user = $this->tokenStorage->getToken()->getUser();
|
||||
|
||||
$event = (new SingleTaskPlaceEvent())
|
||||
->setTask($task)
|
||||
->setAuthor($user)
|
||||
->setTransition($e->getTransition()->getName())
|
||||
->setData([
|
||||
'old_states' => $e->getTransition()->getFroms(),
|
||||
'new_states' => $e->getTransition()->getTos(),
|
||||
'workflow' => $e->getWorkflowName()
|
||||
])
|
||||
;
|
||||
|
||||
$task->getTaskPlaceEvents()->add($event);
|
||||
|
||||
$this->em->persist($event);
|
||||
}
|
||||
|
||||
}
|
55
src/Bundle/ChillTask/Event/TaskEvent.php
Normal file
55
src/Bundle/ChillTask/Event/TaskEvent.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Event;
|
||||
|
||||
use Chill\TaskBundle\Entity\AbstractTask;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class TaskEvent extends Event
|
||||
{
|
||||
const PERSIST = 'chill_task.task_persist';
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AbstractTask
|
||||
*/
|
||||
protected $task;
|
||||
|
||||
public function __construct(AbstractTask $task)
|
||||
{
|
||||
$this->task = $task;
|
||||
}
|
||||
|
||||
public function getTask(): AbstractTask
|
||||
{
|
||||
return $this->task;
|
||||
}
|
||||
|
||||
public function setTask(AbstractTask $task)
|
||||
{
|
||||
$this->task = $task;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
136
src/Bundle/ChillTask/Event/UI/UIEvent.php
Normal file
136
src/Bundle/ChillTask/Event/UI/UIEvent.php
Normal file
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/*
|
||||
*
|
||||
*/
|
||||
namespace Chill\TaskBundle\Event\UI;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Chill\TaskBundle\Entity\AbstractTask;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class UIEvent extends Event
|
||||
{
|
||||
const SHOW_TRANSITION_PAGE = 'chill_task.show_transition_page';
|
||||
const EDIT_FORM = 'chill_task.edit_form';
|
||||
const EDIT_PAGE = 'chill_task.edit_page';
|
||||
|
||||
/**
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $kind;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AbstractTask
|
||||
*/
|
||||
protected $task;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Response|null
|
||||
*/
|
||||
protected $response = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var FormInterface|null
|
||||
*/
|
||||
protected $form = null;
|
||||
|
||||
/**
|
||||
* @var Transition
|
||||
*/
|
||||
protected $transition = null;
|
||||
|
||||
public function __construct($kind, AbstractTask $task)
|
||||
{
|
||||
$this->kind = $kind;
|
||||
$this->task = $task;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKind()
|
||||
{
|
||||
return $this->kind;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return AbstractTask
|
||||
*/
|
||||
public function getTask(): AbstractTask
|
||||
{
|
||||
return $this->task;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return FormInterface|null
|
||||
*/
|
||||
public function getForm()
|
||||
{
|
||||
return $this->form;
|
||||
}
|
||||
|
||||
public function setForm(FormInterface $form)
|
||||
{
|
||||
$this->form = $form;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return Transition
|
||||
*/
|
||||
public function getTransition()
|
||||
{
|
||||
return $this->transition;
|
||||
}
|
||||
|
||||
public function setTransition(Transition $transition)
|
||||
{
|
||||
$this->transition = $transition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getResponse(): Response
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param Response $response
|
||||
* @return $this
|
||||
*/
|
||||
public function setResponse(Response $response)
|
||||
{
|
||||
$this->response = $response;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasResponse()
|
||||
{
|
||||
return $this->response instanceof Response;
|
||||
}
|
||||
|
||||
|
||||
}
|
295
src/Bundle/ChillTask/Form/SingleTaskListType.php
Normal file
295
src/Bundle/ChillTask/Form/SingleTaskListType.php
Normal file
@ -0,0 +1,295 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Chill\TaskBundle\Repository\SingleTaskRepository;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Chill\PersonBundle\Form\Type\PickPersonType;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Chill\PersonBundle\Form\DataTransformer\PersonToIdTransformer;
|
||||
use Chill\TaskBundle\Workflow\TaskWorkflowManager;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class SingleTaskListType extends AbstractType
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $tokenStorage;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TaskWorkflowManager
|
||||
*/
|
||||
protected $taskWorkflowManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
TokenStorageInterface $tokenStorage,
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
TaskWorkflowManager $taskWorkflowManager
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->taskWorkflowManager = $taskWorkflowManager;
|
||||
}
|
||||
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$statuses = [
|
||||
'Tasks not started' => SingleTaskRepository::DATE_STATUS_NOT_STARTED,
|
||||
'Tasks with expired deadline' => SingleTaskRepository::DATE_STATUS_ENDED,
|
||||
'Tasks with warning deadline reached' => SingleTaskRepository::DATE_STATUS_WARNING,
|
||||
'Current tasks' => SingleTaskRepository::DATE_STATUS_CURRENT,
|
||||
'Closed tasks' => 'closed'
|
||||
];
|
||||
|
||||
$builder
|
||||
->add('user_id', ChoiceType::class, [
|
||||
'choices' => $this->getUserChoices($options),
|
||||
'placeholder' => 'Any user',
|
||||
'required' => false,
|
||||
'label' => 'Assignee'
|
||||
])
|
||||
;
|
||||
|
||||
if ($options['add_status']) {
|
||||
$builder
|
||||
->add('status', ChoiceType::class, [
|
||||
'choices' => $statuses,
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'label' => 'status'
|
||||
]);
|
||||
}
|
||||
|
||||
if ($options['add_type']) {
|
||||
$types = $this->getTaskTypesChoices($options);
|
||||
|
||||
if (count($types) > 0) {
|
||||
$builder->add('types', ChoiceType::class, [
|
||||
'choices' => $types,
|
||||
'required' => false,
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'label' => 'Task types'
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($options['person'] === null) {
|
||||
$builder
|
||||
->add('person_id', PickPersonType::class, [
|
||||
'centers' => $this->authorizationHelper
|
||||
->getReachableCenters(
|
||||
$this->tokenStorage->getToken()->getUser(),
|
||||
new Role(TaskVoter::SHOW)
|
||||
),
|
||||
'required' => false,
|
||||
'label' => 'Associated person'
|
||||
])
|
||||
;
|
||||
$reachablesCenters = $this->getReachablesCenters();
|
||||
if (count($reachablesCenters) > 1) {
|
||||
$builder
|
||||
->add('center_id', EntityType::class, [
|
||||
'class' => \Chill\MainBundle\Entity\Center::class,
|
||||
'choices' => $reachablesCenters,
|
||||
'label' => 'Center',
|
||||
'required' => false,
|
||||
'placeholder' => 'All centers'
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// add a hidden field
|
||||
$builder
|
||||
->add('person_id', HiddenType::class);
|
||||
$builder->get('person_id')
|
||||
->addModelTransformer(new PersonToIdTransformer($this->em))
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function getUserChoices($options)
|
||||
{
|
||||
$users = $this->getUsersAssigneedToTask($options);
|
||||
$choices = \array_combine(
|
||||
// get usernames
|
||||
\array_map(function(User $user) { return $user->getUsername(); }, $users),
|
||||
// get ids
|
||||
\array_map(function(User $user) { return $user->getId(); }, $users)
|
||||
);
|
||||
$choices['Unassigned'] = '_unassigned';
|
||||
|
||||
return $choices;
|
||||
}
|
||||
|
||||
protected function getTaskTypesChoices($options)
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$user = $this->tokenStorage->getToken()->getUser();
|
||||
$role = new Role(TaskVoter::SHOW);
|
||||
$centers = $this->authorizationHelper->getReachableCenters($user, $role);
|
||||
|
||||
$qb->select('DISTINCT task.type AS type')
|
||||
->from(SingleTask::class, 'task')
|
||||
->join('task.person', 'person')
|
||||
;
|
||||
|
||||
$i = 0;
|
||||
$orCenters = $qb->expr()->orX();
|
||||
foreach($centers as $center) {
|
||||
$circles = $this->authorizationHelper->getReachableCircles($user, $role, $center);
|
||||
|
||||
if (count($circles) > 0) {
|
||||
$andX = $qb->expr()->andX();
|
||||
$andX
|
||||
->add($qb->expr()->eq('person.center', ':center_'.$i))
|
||||
->add($qb->expr()->in('task.circle', ':circles_'.$i))
|
||||
;
|
||||
$orCenters->add($andX);
|
||||
|
||||
$qb
|
||||
->setParameter('center_'.$i, $center)
|
||||
->setParameter('circles_'.$i, $circles)
|
||||
;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($i > 0) {
|
||||
$qb->where($orCenters);
|
||||
}
|
||||
|
||||
$types = $qb->getQuery()->getResult();
|
||||
|
||||
$choices = [];
|
||||
|
||||
foreach ($types as $row) {
|
||||
$fake = (new SingleTask())->setType($row['type']);
|
||||
$label = $this->taskWorkflowManager->getWorkflowMetadata($fake, 'definition.name');
|
||||
$choices[$label] = $row['type'];
|
||||
}
|
||||
|
||||
return $choices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of user having a task assigned.
|
||||
*
|
||||
* @return User[]
|
||||
*/
|
||||
protected function getUsersAssigneedToTask($options)
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$user = $this->tokenStorage->getToken()->getUser();
|
||||
$role = new Role(TaskVoter::SHOW);
|
||||
$centers = $this->authorizationHelper->getReachableCenters($user, $role);
|
||||
|
||||
$qb->select('DISTINCT user')
|
||||
->from(User::class, 'user')
|
||||
->join(SingleTask::class, 'task', \Doctrine\ORM\Query\Expr\Join::WITH, 'task.assignee = user')
|
||||
->join('task.person', 'person')
|
||||
->where("user.enabled = 'TRUE'")
|
||||
;
|
||||
|
||||
if (NULL !== $options['person']) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->eq('task.person', ':person'))
|
||||
->setParameter('person', $options['person'])
|
||||
;
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
$circleCenterCond = $qb->expr()->orX();
|
||||
foreach ($centers as $center) {
|
||||
$circles = $this->authorizationHelper->getReachableCircles($user, $role, $center);
|
||||
// add condition about person and circle
|
||||
$circleCenterCond->add(
|
||||
$qb->expr()->andX()
|
||||
->add($qb->expr()->eq('person.center', ':center_'.$i))
|
||||
->add($qb->expr()->in('task.circle', ':circles_'.$i))
|
||||
);
|
||||
|
||||
$qb->setParameter('center_'.$i, $center)
|
||||
->setParameter('circles_'.$i, $circles)
|
||||
;
|
||||
// increase counter
|
||||
$i++;
|
||||
}
|
||||
$qb->andWhere($circleCenterCond);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
protected function getReachablesCenters()
|
||||
{
|
||||
$user = $this->tokenStorage->getToken()->getUser();
|
||||
$role = new Role(TaskVoter::SHOW);
|
||||
|
||||
return $this->authorizationHelper->getReachableCenters($user, $role);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setDefined('person')
|
||||
->setDefault('person', null)
|
||||
->setAllowedTypes('person', [Person::class, 'null'])
|
||||
->setDefined('add_status')
|
||||
->setDefault('add_status', false)
|
||||
->setAllowedTypes('add_status', ['bool'])
|
||||
->setDefined('add_type')
|
||||
->setDefault('add_type', false)
|
||||
->setAllowedTypes('add_type', ['bool'])
|
||||
;
|
||||
}
|
||||
}
|
80
src/Bundle/ChillTask/Form/SingleTaskType.php
Normal file
80
src/Bundle/ChillTask/Form/SingleTaskType.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Chill\MainBundle\Form\Type\UserPickerType;
|
||||
use Chill\MainBundle\Form\Type\ScopePickerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
use Chill\MainBundle\Form\Type\DateIntervalType;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class SingleTaskType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('title', TextType::class)
|
||||
->add('description', TextareaType::class, [
|
||||
'required' => false
|
||||
])
|
||||
->add('assignee', UserPickerType::class, [
|
||||
'required' => false,
|
||||
'center' => $options['center'],
|
||||
'role' => $options['role'],
|
||||
'placeholder' => 'Not assigned'
|
||||
])
|
||||
->add('circle', ScopePickerType::class, [
|
||||
'center' => $options['center'],
|
||||
'role' => $options['role']
|
||||
])
|
||||
->add('startDate', ChillDateType::class, [
|
||||
'required' => false
|
||||
])
|
||||
->add('endDate', ChillDateType::class, [
|
||||
'required' => false
|
||||
])
|
||||
->add('warningInterval', DateIntervalType::class, [
|
||||
'required' => false
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setRequired('center')
|
||||
->setAllowedTypes('center', [ Center::class ])
|
||||
->setRequired('role')
|
||||
->setAllowedTypes('role', [ Role::class ])
|
||||
;
|
||||
}
|
||||
}
|
80
src/Bundle/ChillTask/Menu/PersonMenuBuilder.php
Normal file
80
src/Bundle/ChillTask/Menu/PersonMenuBuilder.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Menu;
|
||||
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class PersonMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
protected $authorizationChecker;
|
||||
|
||||
public function __construct(
|
||||
AuthorizationCheckerInterface $authorizationChecker,
|
||||
TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->authorizationChecker = $authorizationChecker;
|
||||
}
|
||||
|
||||
|
||||
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||
{
|
||||
/* @var $person \Chill\PersonBundle\Entity\Person */
|
||||
$person = $parameters['person'] ?? null;
|
||||
|
||||
if ($this->authorizationChecker->isGranted(TaskVoter::SHOW, $person)) {
|
||||
$menu->addChild(
|
||||
$this->translator->trans('Tasks'), [
|
||||
'route' => 'chill_task_singletask_list',
|
||||
'routeParameters' => $menuId === 'person' ?
|
||||
[ 'person_id' => $person->getId() ]
|
||||
:
|
||||
null,
|
||||
])
|
||||
->setExtra('order', 400)
|
||||
;
|
||||
if ($menuId === 'section') {
|
||||
$menu->setExtra('icons', 'tasks');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getMenuIds(): array
|
||||
{
|
||||
return ['person'];
|
||||
}
|
||||
}
|
78
src/Bundle/ChillTask/Menu/SectionMenuBuilder.php
Normal file
78
src/Bundle/ChillTask/Menu/SectionMenuBuilder.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Menu;
|
||||
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
class SectionMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
public $authorizationChecker;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
public $translator;
|
||||
|
||||
public function __construct(
|
||||
AuthorizationCheckerInterface $authorizationChecker,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
$this->authorizationChecker = $authorizationChecker;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||
{
|
||||
if (FALSE === $this->authorizationChecker->isGranted(TaskVoter::SHOW)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu->addChild(
|
||||
$this->translator->trans("Tasks"),
|
||||
[
|
||||
'route' => 'chill_task_singletask_list', [
|
||||
'routeParameters' => [
|
||||
'hide_form' => false
|
||||
]
|
||||
]])
|
||||
->setExtras([
|
||||
'order'=> 50,
|
||||
'icon' => 'exclamation-triangle',
|
||||
'entryclass' => 'user_menu__entry--warning-entry'
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getMenuIds(): array
|
||||
{
|
||||
return [ 'section' ];
|
||||
}
|
||||
}
|
142
src/Bundle/ChillTask/Menu/UserMenuBuilder.php
Normal file
142
src/Bundle/ChillTask/Menu/UserMenuBuilder.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Menu;
|
||||
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Chill\TaskBundle\Templating\UI\CountNotificationTask;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Chill\TaskBundle\Repository\SingleTaskRepository;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
* @var CountNotificationTask
|
||||
*/
|
||||
public $counter;
|
||||
|
||||
/*
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
public $tokenStorage;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
public $translator;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
public $authorizationChecker;
|
||||
|
||||
public function __construct(
|
||||
CountNotificationTask $counter,
|
||||
TokenStorageInterface $tokenStorage,
|
||||
TranslatorInterface $translator,
|
||||
AuthorizationCheckerInterface $authorizationChecker
|
||||
) {
|
||||
$this->counter = $counter;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->translator = $translator;
|
||||
$this->authorizationChecker = $authorizationChecker;
|
||||
}
|
||||
|
||||
public static function getMenuIds(): array
|
||||
{
|
||||
return [ 'user' ];
|
||||
}
|
||||
|
||||
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||
{
|
||||
if (FALSE === $this->authorizationChecker->isGranted(TaskVoter::SHOW)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->tokenStorage->getToken()->getUser();
|
||||
$ended = $this->counter->countNotificationEnded($user);
|
||||
$warning = $this->counter->countNotificationWarning($user);
|
||||
|
||||
if ($ended > 0) {
|
||||
$this->addItemInMenu(
|
||||
$menu,
|
||||
$user,
|
||||
'%number% tasks over deadline',
|
||||
'My tasks over deadline',
|
||||
SingleTaskRepository::DATE_STATUS_ENDED,
|
||||
$ended,
|
||||
-15);
|
||||
}
|
||||
|
||||
if ($warning > 0) {
|
||||
$this->addItemInMenu(
|
||||
$menu,
|
||||
$user,
|
||||
'%number% tasks near deadline',
|
||||
'My tasks near deadline',
|
||||
SingleTaskRepository::DATE_STATUS_WARNING,
|
||||
$warning,
|
||||
-14);
|
||||
}
|
||||
|
||||
$menu->addChild("My tasks", [
|
||||
'route' => 'chill_task_single_my_tasks'
|
||||
])
|
||||
->setExtras([
|
||||
'order' => -10,
|
||||
'icon' => 'tasks'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addItemInMenu(MenuItem $menu, User $u, $message, $title, $status, $number, $order)
|
||||
{
|
||||
if ($number > 0) {
|
||||
$menu->addChild(
|
||||
$this->translator->transChoice($message, $number),
|
||||
[
|
||||
'route' => 'chill_task_singletask_list',
|
||||
'routeParameters' => [
|
||||
'user_id' => $u->getId(),
|
||||
'status' => [
|
||||
$status
|
||||
],
|
||||
'hide_form' => true,
|
||||
'title' => $this->translator->trans($title)
|
||||
]
|
||||
])
|
||||
->setExtras([
|
||||
'order'=> $order,
|
||||
'icon' => 'exclamation-triangle',
|
||||
'entryclass' => 'user_menu__entry--warning-entry'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
13
src/Bundle/ChillTask/Repository/AbstractTaskRepository.php
Normal file
13
src/Bundle/ChillTask/Repository/AbstractTaskRepository.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Repository;
|
||||
|
||||
/**
|
||||
* AbstractTaskRepository
|
||||
*
|
||||
* This class was generated by the Doctrine ORM. Add your own custom
|
||||
* repository methods below.
|
||||
*/
|
||||
abstract class AbstractTaskRepository extends \Doctrine\ORM\EntityRepository
|
||||
{
|
||||
}
|
13
src/Bundle/ChillTask/Repository/RecurringTaskRepository.php
Normal file
13
src/Bundle/ChillTask/Repository/RecurringTaskRepository.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Repository;
|
||||
|
||||
/**
|
||||
* RecurringTaskRepository
|
||||
*
|
||||
* This class was generated by the Doctrine ORM. Add your own custom
|
||||
* repository methods below.
|
||||
*/
|
||||
class RecurringTaskRepository extends \Doctrine\ORM\EntityRepository
|
||||
{
|
||||
}
|
301
src/Bundle/ChillTask/Repository/SingleTaskRepository.php
Normal file
301
src/Bundle/ChillTask/Repository/SingleTaskRepository.php
Normal file
@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
|
||||
/**
|
||||
* SingleTaskRepository
|
||||
*/
|
||||
class SingleTaskRepository extends \Doctrine\ORM\EntityRepository
|
||||
{
|
||||
|
||||
const DATE_STATUS_ENDED = 'ended';
|
||||
const DATE_STATUS_WARNING = 'warning';
|
||||
const DATE_STATUS_CURRENT = 'current';
|
||||
const DATE_STATUS_NOT_STARTED = 'not_started';
|
||||
|
||||
const DATE_STATUSES = [
|
||||
self::DATE_STATUS_ENDED,
|
||||
self::DATE_STATUS_WARNING,
|
||||
self::DATE_STATUS_CURRENT,
|
||||
self::DATE_STATUS_NOT_STARTED
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
public function setAuthorizationHelper(AuthorizationHelper $authorizationHelper)
|
||||
{
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the tasks for given parameters.
|
||||
*
|
||||
* The parameters are describe in @see SingleTaskRepository::filterByParameters.
|
||||
*
|
||||
* @see SingleTaskRepository::filterByParameters
|
||||
* @param array $params
|
||||
* @param User $currentUser
|
||||
* @return int
|
||||
*/
|
||||
public function countByParameters($params, User $currentUser = null)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('st')
|
||||
->select('COUNT(st)');
|
||||
|
||||
$this->buildQuery($qb, $params, $currentUser);
|
||||
|
||||
return (int) $qb
|
||||
->getQuery()
|
||||
->getSingleScalarResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find task for given parameters.
|
||||
*
|
||||
* Available parameters:
|
||||
*
|
||||
* - `person` : filter by person associated with the task ;
|
||||
* - `user` : filter by user that created the task ;
|
||||
* - `scope` : filter by scope associated with the task ;
|
||||
* - `date_status`: type of task. To choose between :
|
||||
* `ended`, `warning`, `current`, `not_started` ;
|
||||
* - `is_closed`: boolean. Indicate if the tasks must be closed (true) or
|
||||
* opened
|
||||
* - `types`: string[] an array of task types
|
||||
*
|
||||
* @param type $params
|
||||
* @param User $currentUser
|
||||
* @param int $firstResult
|
||||
* @param int $maxResults
|
||||
* @return type
|
||||
*/
|
||||
public function findByParameters($params, User $currentUser, int $firstResult = 0, int $maxResults = 50)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('st');
|
||||
|
||||
$this->buildQuery($qb, $params, $currentUser);
|
||||
|
||||
$qb
|
||||
->setMaxResults($maxResults)
|
||||
->setFirstResult($firstResult)
|
||||
;
|
||||
|
||||
return $qb
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
protected function buildQuery(QueryBuilder $qb, $params, User $currentUser = null)
|
||||
{
|
||||
if (NULL !== $currentUser) {
|
||||
$this->buildACLQuery($qb, $currentUser);
|
||||
}
|
||||
|
||||
if (\array_key_exists('person', $params) and !empty($params['person'])) {
|
||||
$qb->andWhere($qb->expr()->eq('st.person', ':person'));
|
||||
$qb->setParameter('person', $params['person']);
|
||||
} elseif (\array_key_exists('center', $params)) {
|
||||
if ($params['center'] instanceof Center) {
|
||||
$qb->join('st.person', 'person');
|
||||
$qb->andWhere($qb->expr()->eq('person.center', ':center'));
|
||||
$qb->setParameter('center', $params['center']);
|
||||
} else {
|
||||
throw new \UnexpectedValueException("params 'center' should be an instance of ".Center::class);
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists('unassigned', $params) and $params['unassigned'] === true) {
|
||||
if (\array_key_exists('user', $params) and !empty($params['user'])) {
|
||||
throw new \UnexpectedValueException("You should not require for "
|
||||
. "unassigned tasks and tasks assigned to some user.");
|
||||
}
|
||||
|
||||
$qb->andWhere($qb->expr()->isNull('st.assignee'));
|
||||
}
|
||||
|
||||
if (\array_key_exists('user', $params) and !empty($params['user'])) {
|
||||
$qb->andWhere($qb->expr()->eq('st.assignee', ':user'));
|
||||
$qb->setParameter('user', $params['user']);
|
||||
}
|
||||
|
||||
if (\array_key_exists('scope', $params) and !empty($params['scope'])) {
|
||||
$qb->andWhere($qb->expr()->eq('st.circle', ':scope'));
|
||||
$qb->setParameter('scope', $params['scope']);
|
||||
}
|
||||
|
||||
if (\array_key_exists('types', $params) && $params['types'] !== NULL) {
|
||||
if (count($params['types']) > 0) {
|
||||
$qb->andWhere($qb->expr()->in('st.type', ':types'));
|
||||
$qb->setParameter('types', $params['types']);
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists('date_status', $params) and !empty($params['date_status'])) {
|
||||
$this->addTypeFilter($qb, $params);
|
||||
}
|
||||
|
||||
if (\array_key_exists('is_closed', $params)) {
|
||||
$qb->andWhere($this->buildIsClosed($qb, !$params['is_closed']));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function addTypeFilter(QueryBuilder $qb, $params)
|
||||
{
|
||||
$andWhere = $qb->expr()->andX();
|
||||
|
||||
switch ($params['date_status']) {
|
||||
case self::DATE_STATUS_ENDED:
|
||||
$andWhere
|
||||
->add($this->buildNowIsAfterEndDate($qb))
|
||||
;
|
||||
break;
|
||||
|
||||
case self::DATE_STATUS_WARNING:
|
||||
$andWhere
|
||||
->add($this->buildNowIsAfterEndDate($qb, true))
|
||||
->add($this->buildNowIsAfterWarningDate($qb))
|
||||
;
|
||||
break;
|
||||
|
||||
case self::DATE_STATUS_CURRENT:
|
||||
// st.endDate is NULL or (st.endDate is not null and st.endDate < now))
|
||||
$andWhere
|
||||
->add($this->buildNowIsAfterEndDate($qb, true))
|
||||
->add($this->buildNowIsAfterWarningDate($qb, true))
|
||||
->add($this->buildNowIsAfterStartDate($qb, false))
|
||||
;
|
||||
break;
|
||||
|
||||
case self::DATE_STATUS_NOT_STARTED:
|
||||
$andWhere
|
||||
->add($this->buildNowIsAfterEndDate($qb, true))
|
||||
->add($this->buildNowIsAfterWarningDate($qb, true))
|
||||
->add($this->buildNowIsAfterStartDate($qb, true))
|
||||
;
|
||||
}
|
||||
$qb->setParameter('now', new \DateTime('today'), Type::DATE);
|
||||
$qb->andWhere($andWhere);
|
||||
}
|
||||
|
||||
private function buildNowIsAfterEndDate(QueryBuilder $qb, $negative = false)
|
||||
{
|
||||
if ($negative === false) {
|
||||
return $qb->expr()->andX()
|
||||
->add($qb->expr()->isNotNull('st.endDate'))
|
||||
->add($qb->expr()->lte('st.endDate', ':now'))
|
||||
;
|
||||
} else {
|
||||
return $qb->expr()->orX()
|
||||
->add(
|
||||
$qb->expr()->andX()
|
||||
->add($qb->expr()->isNotNull('st.endDate'))
|
||||
->add($qb->expr()->gt('st.endDate', ':now'))
|
||||
)
|
||||
->add($qb->expr()->isNull('st.endDate'))
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildNowIsAfterWarningDate(QueryBuilder $qb, bool $negative = false)
|
||||
{
|
||||
if ($negative === false) {
|
||||
return $qb->expr()->andX()
|
||||
->add($qb->expr()->lte(
|
||||
$qb->expr()->diff('st.endDate', 'st.warningInterval'), ':now'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return $qb->expr()->orX()
|
||||
->add(
|
||||
$qb->expr()->andX()
|
||||
->add($qb->expr()->isNotNull('st.endDate'))
|
||||
->add($qb->expr()->isNotNull('st.warningInterval'))
|
||||
->add($qb->expr()->gt(
|
||||
$qb->expr()->diff('st.endDate', 'st.warningInterval'),
|
||||
':now'
|
||||
)
|
||||
)
|
||||
)
|
||||
->add($qb->expr()->isNull('st.endDate'))
|
||||
->add($qb->expr()->isNull('st.warningInterval'))
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildNowIsAfterStartDate(QueryBuilder $qb, bool $negative = false)
|
||||
{
|
||||
if ($negative === false) {
|
||||
return $qb->expr()->orX()
|
||||
->add($qb->expr()->lte('st.startDate', ':now'))
|
||||
->add($qb->expr()->isNull('st.startDate'))
|
||||
;
|
||||
} else {
|
||||
return
|
||||
$qb->expr()->andX()
|
||||
->add($qb->expr()->gt('st.startDate', ':now'))
|
||||
->add($qb->expr()->isNotNull('st.startDate'))
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildIsClosed(QueryBuilder $qb, bool $negative = false)
|
||||
{
|
||||
if ($negative === false) {
|
||||
return $qb->expr()->eq('st.closed', "'TRUE'");
|
||||
} else {
|
||||
return $qb->expr()->eq('st.closed', "'FALSE'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function buildACLQuery(QueryBuilder $qb, User $currentUser)
|
||||
{
|
||||
if (NULL === $this->authorizationHelper) {
|
||||
throw new \LogicException("Injecting the authorization helper is "
|
||||
. "required to run this query. Please use dependency injection "
|
||||
. "to initialize this repository or use the method "
|
||||
. "`setAuthorizationHelper`");
|
||||
}
|
||||
|
||||
$role = new Role(TaskVoter::SHOW);
|
||||
$qb->join('st.person', 'p');
|
||||
|
||||
$centers = $this->authorizationHelper
|
||||
->getReachableCenters($currentUser, $role)
|
||||
;
|
||||
|
||||
$i = 0;
|
||||
$where = $qb->expr()->orX();
|
||||
|
||||
foreach($centers as $center) {
|
||||
$circles = $this->authorizationHelper
|
||||
->getReachableCircles($currentUser, $role, $center);
|
||||
|
||||
$centerWhere = $qb->expr()->andX();
|
||||
|
||||
$centerWhere->add($qb->expr()->eq('p.center', ':center_'.$i));
|
||||
$qb->setParameter('center_'.$i, $center);
|
||||
$centerWhere->add($qb->expr()->in('st.circle', ':circles_'.$i));
|
||||
$qb->setParameter('circles_'.$i, $circles);
|
||||
$where->add($centerWhere);
|
||||
$i ++;
|
||||
}
|
||||
|
||||
$qb->where($where);
|
||||
}
|
||||
}
|
3
src/Bundle/ChillTask/Resources/config/routing.yml
Normal file
3
src/Bundle/ChillTask/Resources/config/routing.yml
Normal file
@ -0,0 +1,3 @@
|
||||
chill_task_controllers:
|
||||
resource: "@ChillTaskBundle/Controller"
|
||||
type: annotation
|
2
src/Bundle/ChillTask/Resources/config/services.yml
Normal file
2
src/Bundle/ChillTask/Resources/config/services.yml
Normal file
@ -0,0 +1,2 @@
|
||||
services:
|
||||
|
@ -0,0 +1,9 @@
|
||||
services:
|
||||
Chill\TaskBundle\Controller\:
|
||||
resource: '../../../Controller'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
Chill\TaskBundle\Controller\SingleTaskController:
|
||||
arguments:
|
||||
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
|
||||
tags: ['controller.service_arguments']
|
7
src/Bundle/ChillTask/Resources/config/services/event.yml
Normal file
7
src/Bundle/ChillTask/Resources/config/services/event.yml
Normal file
@ -0,0 +1,7 @@
|
||||
services:
|
||||
Chill\TaskBundle\Event\Lifecycle\TaskLifecycleEvent:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
|
||||
tags:
|
||||
- { name: kernel.event_subscriber }
|
@ -0,0 +1,4 @@
|
||||
services:
|
||||
Chill\TaskBundle\DataFixtures\ORM\:
|
||||
resource: ../../../DataFixtures/ORM
|
||||
tags: [ 'doctrine.fixture.orm' ]
|
9
src/Bundle/ChillTask/Resources/config/services/form.yml
Normal file
9
src/Bundle/ChillTask/Resources/config/services/form.yml
Normal file
@ -0,0 +1,9 @@
|
||||
services:
|
||||
Chill\TaskBundle\Form\SingleTaskListType:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
|
||||
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
|
||||
$taskWorkflowManager: '@Chill\TaskBundle\Workflow\TaskWorkflowManager'
|
||||
tags:
|
||||
- { name: form.type }
|
23
src/Bundle/ChillTask/Resources/config/services/menu.yml
Normal file
23
src/Bundle/ChillTask/Resources/config/services/menu.yml
Normal file
@ -0,0 +1,23 @@
|
||||
services:
|
||||
Chill\TaskBundle\Menu\UserMenuBuilder:
|
||||
arguments:
|
||||
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
|
||||
$counter: '@Chill\TaskBundle\Templating\UI\CountNotificationTask'
|
||||
$translator: '@Symfony\Component\Translation\TranslatorInterface'
|
||||
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
|
||||
tags:
|
||||
- { name: 'chill.menu_builder' }
|
||||
|
||||
Chill\TaskBundle\Menu\PersonMenuBuilder:
|
||||
arguments:
|
||||
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
|
||||
$translator: '@Symfony\Component\Translation\TranslatorInterface'
|
||||
tags:
|
||||
- { name: 'chill.menu_builder' }
|
||||
|
||||
Chill\TaskBundle\Menu\SectionMenuBuilder:
|
||||
arguments:
|
||||
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
|
||||
$translator: '@Symfony\Component\Translation\TranslatorInterface'
|
||||
tags:
|
||||
- { name: 'chill.menu_builder' }
|
@ -0,0 +1,11 @@
|
||||
services:
|
||||
chill_task.single_task_repository:
|
||||
class: Chill\TaskBundle\Repository\SingleTaskRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments:
|
||||
- 'Chill\TaskBundle\Entity\SingleTask'
|
||||
calls:
|
||||
- method: setAuthorizationHelper
|
||||
arguments:
|
||||
- "@chill.main.security.authorization.helper"
|
||||
Chill\TaskBundle\Repository\SingleTaskRepository: '@chill_task.single_task_repository'
|
11
src/Bundle/ChillTask/Resources/config/services/security.yml
Normal file
11
src/Bundle/ChillTask/Resources/config/services/security.yml
Normal file
@ -0,0 +1,11 @@
|
||||
services:
|
||||
chill_task.task_voter:
|
||||
class: Chill\TaskBundle\Security\Authorization\TaskVoter
|
||||
arguments:
|
||||
- "@security.access.decision_manager"
|
||||
- "@chill.main.security.authorization.helper"
|
||||
- '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
|
||||
- "@logger"
|
||||
tags:
|
||||
- { name: security.voter }
|
||||
- { name: chill.role }
|
@ -0,0 +1,13 @@
|
||||
services:
|
||||
Chill\TaskBundle\Templating\TaskTwigExtension:
|
||||
arguments:
|
||||
$taskWorkflowManager: '@Chill\TaskBundle\Workflow\TaskWorkflowManager'
|
||||
tags:
|
||||
- { name: 'twig.extension' }
|
||||
|
||||
Chill\TaskBundle\Templating\UI\CountNotificationTask:
|
||||
arguments:
|
||||
$singleTaskRepository: '@Chill\TaskBundle\Repository\SingleTaskRepository'
|
||||
$cachePool: '@cache.user_data'
|
||||
tags:
|
||||
- { name: chill.count_notification.user }
|
16
src/Bundle/ChillTask/Resources/config/services/timeline.yml
Normal file
16
src/Bundle/ChillTask/Resources/config/services/timeline.yml
Normal file
@ -0,0 +1,16 @@
|
||||
services:
|
||||
Chill\TaskBundle\Timeline\TaskLifeCycleEventTimelineProvider:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
$registry: '@Symfony\Component\Workflow\Registry'
|
||||
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
|
||||
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
|
||||
tags:
|
||||
- { name: 'chill.timeline', context: 'person' }
|
||||
|
||||
Chill\TaskBundle\Timeline\SingleTaskTaskLifeCycleEventTimelineProvider:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
$registry: '@Symfony\Component\Workflow\Registry'
|
||||
tags:
|
||||
- { name: 'chill.timeline', context: 'task' }
|
12
src/Bundle/ChillTask/Resources/config/services/workflow.yml
Normal file
12
src/Bundle/ChillTask/Resources/config/services/workflow.yml
Normal file
@ -0,0 +1,12 @@
|
||||
services:
|
||||
Chill\TaskBundle\Workflow\TaskWorkflowManager: ~
|
||||
|
||||
Chill\TaskBundle\Workflow\Definition\DefaultTaskDefinition:
|
||||
tags:
|
||||
- { name: 'chill_task.workflow_definition' }
|
||||
|
||||
Chill\TaskBundle\Workflow\Event\DefaultTaskGuardEvent:
|
||||
arguments:
|
||||
- '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
|
||||
tags:
|
||||
- { name: kernel.event_subscriber }
|
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Create a table for Single task.
|
||||
*/
|
||||
class Version20180413135614 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('CREATE SCHEMA chill_task');
|
||||
$this->addSql('CREATE SEQUENCE chill_task.single_task_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE chill_task.single_task (id INT NOT NULL, assignee_id INT DEFAULT NULL, person_id INT DEFAULT NULL, circle_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, current_states JSONB NOT NULL, title TEXT NOT NULL, description TEXT NOT NULL, start_date DATE DEFAULT NULL, end_date DATE DEFAULT NULL, warning_interval INTERVAL DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_194CB3D859EC7D60 ON chill_task.single_task (assignee_id)');
|
||||
$this->addSql('CREATE INDEX IDX_194CB3D8217BBB47 ON chill_task.single_task (person_id)');
|
||||
$this->addSql('CREATE INDEX IDX_194CB3D870EE2FF6 ON chill_task.single_task (circle_id)');
|
||||
$this->addSql('COMMENT ON COLUMN chill_task.single_task.warning_interval IS \'(DC2Type:dateinterval)\'');
|
||||
$this->addSql('ALTER TABLE chill_task.single_task ADD CONSTRAINT FK_194CB3D859EC7D60 FOREIGN KEY (assignee_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_task.single_task ADD CONSTRAINT FK_194CB3D8217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_task.single_task ADD CONSTRAINT FK_194CB3D870EE2FF6 FOREIGN KEY (circle_id) REFERENCES scopes (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('DROP SCHEMA chill_task CASCADE');
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Create recurring task
|
||||
*/
|
||||
class Version20180413201023 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('CREATE SEQUENCE chill_task.recurring_task_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE chill_task.recurring_task (id INT NOT NULL, assignee_id INT DEFAULT NULL, person_id INT DEFAULT NULL, circle_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, current_states JSONB NOT NULL, title TEXT NOT NULL, description TEXT NOT NULL, first_occurence_end_date DATE NOT NULL, last_occurence_end_date DATE NOT NULL, occurence_frequency VARCHAR(255) NOT NULL, occurence_start_date INTERVAL NOT NULL, occurence_warning_interval INTERVAL DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_9F663B9059EC7D60 ON chill_task.recurring_task (assignee_id)');
|
||||
$this->addSql('CREATE INDEX IDX_9F663B90217BBB47 ON chill_task.recurring_task (person_id)');
|
||||
$this->addSql('CREATE INDEX IDX_9F663B9070EE2FF6 ON chill_task.recurring_task (circle_id)');
|
||||
$this->addSql('COMMENT ON COLUMN chill_task.recurring_task.occurence_start_date IS \'(DC2Type:dateinterval)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_task.recurring_task.occurence_warning_interval IS \'(DC2Type:dateinterval)\'');
|
||||
$this->addSql('ALTER TABLE chill_task.recurring_task ADD CONSTRAINT FK_9F663B9059EC7D60 FOREIGN KEY (assignee_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_task.recurring_task ADD CONSTRAINT FK_9F663B90217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_task.recurring_task ADD CONSTRAINT FK_9F663B9070EE2FF6 FOREIGN KEY (circle_id) REFERENCES scopes (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_task.single_task ADD recurringTask_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_task.single_task ADD CONSTRAINT FK_194CB3D840868C31 FOREIGN KEY (recurringTask_id) REFERENCES chill_task.recurring_task (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_194CB3D840868C31 ON chill_task.single_task (recurringTask_id)');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE chill_task.single_task DROP CONSTRAINT FK_194CB3D840868C31');
|
||||
$this->addSql('DROP SEQUENCE chill_task.recurring_task_id_seq CASCADE');
|
||||
|
||||
$this->addSql('DROP TABLE chill_task.recurring_task');
|
||||
$this->addSql('ALTER TABLE chill_task.single_task DROP recurringTask_id');
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Add a column 'closed' on tasks
|
||||
*/
|
||||
class Version20180426093011 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema)
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE chill_task.single_task ADD closed BOOLEAN DEFAULT \'false\' NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_task.recurring_task ADD closed BOOLEAN DEFAULT \'false\' NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema)
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE chill_task.recurring_task DROP closed');
|
||||
$this->addSql('ALTER TABLE chill_task.single_task DROP closed');
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Create task place events table.
|
||||
*/
|
||||
class Version20180502194119 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema)
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('CREATE SEQUENCE chill_task.single_task_place_event_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE chill_task.single_task_place_event (id INT NOT NULL, author_id INT DEFAULT NULL, task_id INT DEFAULT NULL, datetime TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, transition VARCHAR(255) NOT NULL, data JSONB NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_D459EBEEF675F31B ON chill_task.single_task_place_event (author_id)');
|
||||
$this->addSql('CREATE INDEX IDX_D459EBEE8DB60186 ON chill_task.single_task_place_event (task_id)');
|
||||
$this->addSql('COMMENT ON COLUMN chill_task.single_task_place_event.datetime IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_task.single_task_place_event ADD CONSTRAINT FK_D459EBEEF675F31B FOREIGN KEY (author_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_task.single_task_place_event ADD CONSTRAINT FK_D459EBEE8DB60186 FOREIGN KEY (task_id) REFERENCES chill_task.single_task (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema)
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('DROP SEQUENCE chill_task.single_task_place_event_id_seq CASCADE');
|
||||
$this->addSql('DROP TABLE chill_task.single_task_place_event');
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Add index on single_task_place_events
|
||||
*/
|
||||
final class Version20181113161925 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('CREATE INDEX transition_task_date ON chill_task.single_task_place_event (task_id, transition, datetime)');
|
||||
$this->addSql('CREATE INDEX transition_task ON chill_task.single_task_place_event (task_id, transition)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('DROP INDEX transition_task_date');
|
||||
$this->addSql('DROP INDEX transition_task');
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Add index on single task
|
||||
*/
|
||||
final class Version20181113164108 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('CREATE INDEX by_type ON chill_task.single_task (type)');
|
||||
$this->addSql('CREATE INDEX by_current_state ON chill_task.single_task USING GIN (current_states)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('DROP INDEX by_type');
|
||||
$this->addSql('DROP INDEX by_current_state');
|
||||
}
|
||||
}
|
3
src/Bundle/ChillTask/Resources/public/index.js
Normal file
3
src/Bundle/ChillTask/Resources/public/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
// this file loads all assets from the Chill task bundle
|
||||
require('./sass/_task.scss');
|
||||
|
53
src/Bundle/ChillTask/Resources/public/sass/_task-list.scss
Normal file
53
src/Bundle/ChillTask/Resources/public/sass/_task-list.scss
Normal file
@ -0,0 +1,53 @@
|
||||
table.chill-task-list {
|
||||
.chill-task-list__row > div {
|
||||
margin-bottom: 0.50rem;
|
||||
}
|
||||
|
||||
.chill-task-list__row__title {
|
||||
font-weight: bold;
|
||||
font-size: 1.40rem;
|
||||
}
|
||||
|
||||
.chill-task-list__row__type {
|
||||
font-variant: small-caps;
|
||||
display: inline;
|
||||
padding: 0.05rem .15rem;
|
||||
font-size: 0.88rem;
|
||||
font-weight: light;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border: 1px solid var(--chill-dark-gray);
|
||||
color: var(--chill-dark-gray);
|
||||
}
|
||||
|
||||
.chill-task-list__row__person-for {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chill-task-list__row__assignee {
|
||||
display: inline;
|
||||
|
||||
}
|
||||
|
||||
.chill_task-list__row__assignee_by {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chill-task-list__row__dates {
|
||||
& > ul {
|
||||
display: inline;
|
||||
list-style: none;
|
||||
|
||||
& > li {
|
||||
display: inline;
|
||||
margin-right: 0.25rem;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
.task-status {
|
||||
&.box {
|
||||
font-variant: small-caps;
|
||||
display: inline;
|
||||
padding: .2em .6em .3em;
|
||||
font-size: 0.88rem;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: .25em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.type-task_default {
|
||||
// 'new', 'in_progress', 'closed', 'canceled'
|
||||
&.place-new {
|
||||
background-color: var(--chill-yellow);
|
||||
}
|
||||
|
||||
&.place-in_progress {
|
||||
background-color: var(--chill-green);
|
||||
}
|
||||
|
||||
&.place-closed {
|
||||
background-color: var(--chill-blue);
|
||||
}
|
||||
|
||||
&.place-canceled {
|
||||
background-color: var(--chill-beige);
|
||||
}
|
||||
}
|
||||
}
|
88
src/Bundle/ChillTask/Resources/public/sass/_task.scss
Normal file
88
src/Bundle/ChillTask/Resources/public/sass/_task.scss
Normal file
@ -0,0 +1,88 @@
|
||||
@import '../../../../main/Resources/public/sass/custom/config/colors';
|
||||
@import "_task-statuses.scss";
|
||||
@import "_task-list.scss";
|
||||
|
||||
div#single_task_warningInterval {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
div.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
& > div:first-child {
|
||||
display:none; /* Hide the label container */
|
||||
}
|
||||
& > div {
|
||||
padding-right: 0;
|
||||
width: 7em;
|
||||
}
|
||||
input[type="text"] {
|
||||
height: 35px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bt-task-label {
|
||||
display: inline-flex;
|
||||
margin-bottom: 0.2em;
|
||||
font-size: 0.8em;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.bt-task {
|
||||
display: inline-block;
|
||||
height: 0;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.bt-task-exchange {
|
||||
background: $chill-green;
|
||||
color: #fff;
|
||||
}
|
||||
.bt-task-exchange:hover {
|
||||
background: $chill-green-dark;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bt-task-exchange::before {
|
||||
@extend .bt-task;
|
||||
width: 10px;
|
||||
content: url(../svg/exchange-alt.svg);
|
||||
}
|
||||
|
||||
.bt-task-start::before {
|
||||
@extend .bt-task;
|
||||
content: url(../svg/play.svg);
|
||||
}
|
||||
|
||||
.bt-task-cancel::before {
|
||||
@extend .bt-task;
|
||||
content: url(../svg/archive.svg);
|
||||
}
|
||||
|
||||
.bt-task-close::before {
|
||||
@extend .bt-task;
|
||||
content: url(../svg/check.svg);
|
||||
}
|
||||
|
||||
.bt-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bt-dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
a {
|
||||
margin: 0.4em;
|
||||
}
|
||||
}
|
||||
|
||||
.bt-dropdown:hover .bt-dropdown-content {
|
||||
display: inline-block;
|
||||
width: min-content;
|
||||
}
|
1
src/Bundle/ChillTask/Resources/public/svg/archive.svg
Normal file
1
src/Bundle/ChillTask/Resources/public/svg/archive.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M32 448c0 17.7 14.3 32 32 32h384c17.7 0 32-14.3 32-32V160H32v288zm160-212c0-6.6 5.4-12 12-12h104c6.6 0 12 5.4 12 12v8c0 6.6-5.4 12-12 12H204c-6.6 0-12-5.4-12-12v-8zM480 32H32C14.3 32 0 46.3 0 64v48c0 8.8 7.2 16 16 16h480c8.8 0 16-7.2 16-16V64c0-17.7-14.3-32-32-32z"/></svg>
|
After Width: | Height: | Size: 344 B |
1
src/Bundle/ChillTask/Resources/public/svg/check.svg
Normal file
1
src/Bundle/ChillTask/Resources/public/svg/check.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/></svg>
|
After Width: | Height: | Size: 354 B |
52
src/Bundle/ChillTask/Resources/public/svg/exchange-alt.svg
Normal file
52
src/Bundle/ChillTask/Resources/public/svg/exchange-alt.svg
Normal file
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 512 512"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="exchange-alt.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1313"
|
||||
inkscape:window-height="744"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.4609375"
|
||||
inkscape:cx="256"
|
||||
inkscape:cy="256"
|
||||
inkscape:window-x="53"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M0 168v-16c0-13.255 10.745-24 24-24h360V80c0-21.367 25.899-32.042 40.971-16.971l80 80c9.372 9.373 9.372 24.569 0 33.941l-80 80C409.956 271.982 384 261.456 384 240v-48H24c-13.255 0-24-10.745-24-24zm488 152H128v-48c0-21.314-25.862-32.08-40.971-16.971l-80 80c-9.372 9.373-9.372 24.569 0 33.941l80 80C102.057 463.997 128 453.437 128 432v-48h360c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24z"
|
||||
id="path4"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
1
src/Bundle/ChillTask/Resources/public/svg/play.svg
Normal file
1
src/Bundle/ChillTask/Resources/public/svg/play.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"/></svg>
|
After Width: | Height: | Size: 191 B |
105
src/Bundle/ChillTask/Resources/translations/messages.fr.yml
Normal file
105
src/Bundle/ChillTask/Resources/translations/messages.fr.yml
Normal file
@ -0,0 +1,105 @@
|
||||
Tasks: 'Tâches'
|
||||
'New task': 'Nouvelle tâche'
|
||||
'Add a new task': 'Ajouter une nouvelle tâche'
|
||||
Title: Titre
|
||||
Description: Description
|
||||
Assignee: 'Personne assignée'
|
||||
Scope: Cercle
|
||||
'Start date': 'Date de début'
|
||||
'End date': "Date d'échéance"
|
||||
'Warning date': "Date d'avertissement"
|
||||
'Warning interval': "Délai d'avertissement avant la date d'échéance"
|
||||
'Unknown dates': 'Dates non spécifiées'
|
||||
'N': ''
|
||||
'Unit': ''
|
||||
Task: Tâche
|
||||
Details: Détails
|
||||
Person: Personne
|
||||
Scope: Cercle
|
||||
Date: Date
|
||||
Dates: Dates
|
||||
User: Utilisateur
|
||||
'Task list': 'Liste des tâches'
|
||||
'Tasks with expired deadline': "Tâches avec une date d'échéance dépassée"
|
||||
'Tasks with warning deadline reached': "Tâches avec une date d'avertissement atteinte"
|
||||
'Current tasks': 'Tâches en cours'
|
||||
'Closed tasks': Tâches terminées
|
||||
'Tasks not started': 'Tâches non commencées'
|
||||
'Task start date': 'Date de début'
|
||||
'Task warning date': "Date d'avertissement"
|
||||
'Task end date': "Date d'échéance"
|
||||
'Start': 'Début'
|
||||
'Warning': 'Avertissement'
|
||||
'End': 'Échéance'
|
||||
'Task type': 'Type'
|
||||
'Task status': 'Statut'
|
||||
'Edit the task': 'Modifier la tâche'
|
||||
'Edit task': 'Modifier la tâche'
|
||||
'Save task': 'Enregistrer la tâche'
|
||||
'View the task': 'Voir la tâche'
|
||||
'Update the task': 'Mettre à jour la tâche'
|
||||
'Remove task': 'Supprimer la tâche'
|
||||
'Delete': 'Supprimer'
|
||||
'Change task status': 'Changer le statut'
|
||||
'Are you sure you want to remove the task about "%name%" ?': 'Êtes-vous sûr·e de vouloir supprimer la tâche de "%name%"?'
|
||||
'See more': 'Voir plus'
|
||||
'Associated tasks': 'Tâches associées'
|
||||
'My tasks': 'Mes tâches'
|
||||
'No description': 'Pas de description'
|
||||
'No dates specified': 'Dates non spécifiées'
|
||||
'No one assignee': 'Aucune personne assignée'
|
||||
'Task types': Types de tâches
|
||||
Days: Jour(s)
|
||||
Weeks: Semaine(s)
|
||||
Months: Mois
|
||||
Year: Année(s)
|
||||
Filter the tasks: Filtrer les tâches
|
||||
Filter: Filtrer
|
||||
Any user: Tous les utilisateurs
|
||||
Unassigned: Non assigné
|
||||
Associated person: Personne associée
|
||||
Default task: Tâche par défaut
|
||||
Not assigned: Aucun utilisateur assigné
|
||||
For person: Pour
|
||||
By: Par
|
||||
|
||||
# transitions - default task definition
|
||||
'new': 'nouvelle'
|
||||
'in_progress': 'en cours'
|
||||
'closed': 'fermée'
|
||||
'canceled': 'supprimée'
|
||||
start: démarrer
|
||||
close: clotûrer
|
||||
cancel: annuler
|
||||
Start_verb: Démarrer
|
||||
Close_verb: Clotûrer
|
||||
Set this task to cancel state: Marquer cette tâche comme annulée
|
||||
'%user% has closed the task': %user% a fermé la tâche
|
||||
'%user% has canceled the task': %user% a annulé la tâche
|
||||
'%user% has started the task': %user% a commencé la tâche
|
||||
'%user% has created the task': %user% a introduit la tâche
|
||||
Are you sure you want to close this task ?: Êtes-vous sûrs de vouloir clotûrer cette tâche ?
|
||||
Are you sure you want to cancel this task ?: Êtes-vous sûrs de vouloir annuler cette tâche ?
|
||||
Are you sure you want to start this task ?: Êtes-vous sûrs de vouloir démarrer cette tâche ?
|
||||
|
||||
#Flash messages
|
||||
'The task is created': 'La tâche a été créée'
|
||||
'There is no tasks.': Aucune tâche.
|
||||
'The task has been successfully removed.': 'La tâche a bien été supprimée'
|
||||
'This form contains errors': 'Ce formulaire contient des erreurs'
|
||||
'The task has been updated': 'La tâche a été mise à jour'
|
||||
'The transition is successfully applied': 'La transition a bien été effectuée'
|
||||
'The transition could not be applied': "La transition n'a pas pu être appliquée"
|
||||
|
||||
#widget
|
||||
'%number% tasks over deadline': '{0} Aucune tâche dépassée|{1} Une tâche dépassée | ]1,Inf[ %count% tâches dépassées'
|
||||
'%number% tasks near deadline': '{0} Aucune tâche en rappel|{1} Une tâche en rappel | ]1,Inf[ %count% tâches en rappel'
|
||||
|
||||
#title
|
||||
My tasks near deadline: Mes tâches à échéance proche
|
||||
My tasks over deadline: Mes tâches à échéance dépassée
|
||||
|
||||
#transition page
|
||||
Apply transition on task <em>%title%</em>: Appliquer la transition sur la tâche <em>%title%</em>
|
||||
|
||||
All centers: Tous les centres
|
@ -0,0 +1,4 @@
|
||||
The start date must be before the end date: La date de début doit être avant la date d'échéance
|
||||
This form contains errors: Le formulaire contient des erreurs
|
||||
The start date must be before warning date: La date de début doit être avant le délai d'avertissement
|
||||
An end date is required if a warning interval is set: Une date d'échéance est requise lorsqu'un délai de rappel est fixé
|
240
src/Bundle/ChillTask/Resources/views/SingleTask/_list.html.twig
Normal file
240
src/Bundle/ChillTask/Resources/views/SingleTask/_list.html.twig
Normal file
@ -0,0 +1,240 @@
|
||||
{% macro date_status(title, tasks, count, paginator, status, isSingleStatus, person, user) %}
|
||||
{% if tasks|length > 0 %}
|
||||
<h3>{{ title|trans }}</h3>
|
||||
|
||||
<table class="records_list chill-task-list">
|
||||
<tbody>
|
||||
{% for task in tasks %}
|
||||
<tr>
|
||||
<td class="chill-task-list__row">
|
||||
|
||||
<div>
|
||||
<span class="chill-task-list__row__title">{{ task.title }}</span>
|
||||
</div>
|
||||
|
||||
{% if person is null %}
|
||||
<div>
|
||||
<span class="chill-task-list__row__person-for">{{ 'For person'|trans }} :</span> <span class="chill-task-list__row__person"><a href="{{ path('chill_person_view', {person_id : task.person.Id}) }}">{{ task.person}}</a></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<span class="chill-task-list__row__type">{{ task_workflow_metadata(task, 'definition.name')|trans }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
{% for place in workflow_marked_places(task) %}
|
||||
<span class="task-status box type-{{ task.type }} place-{{ place }}">{{ place|trans }}</span>
|
||||
{% endfor %}
|
||||
{% if task.assignee is not null %}
|
||||
<div class="chill-task-list__row__assignee"><span class="chill_task-list__row__assignee_by">{{ 'By'|trans }} :</span> {{ task.assignee.username }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{% if task.startDate is not null or task.warningDate is not null or task.endDate is not null %}
|
||||
<div class="chill-task-list__row__dates">
|
||||
<ul class="record_actions_column">
|
||||
{% if task.startDate is not null %}
|
||||
<li title="{{ 'Start'|trans|escape('html_attr') }}">
|
||||
<i class="fa fa-play" ></i> {{ task.startDate|localizeddate('medium', 'none') }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if task.warningDate is not null %}
|
||||
<li title="{{ 'Warning'|trans|escape('html_attr') }}">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ task.warningDate|localizeddate('medium', 'none') }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if task.endDate is not null %}
|
||||
<li title="{{ 'End'|trans|escape('html_attr') }}">
|
||||
<i class="fa fa-hourglass-end"></i> {{ task.endDate|localizeddate('medium', 'none') }}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<ul class="record_actions">
|
||||
{% if workflow_transitions(task)|length > 0 %}
|
||||
<li>
|
||||
<div class="bt-dropdown">
|
||||
<a href="" class="sc-button bt-task-exchange"> </a>
|
||||
<div class="bt-dropdown-content">
|
||||
{% for transition in workflow_transitions(task) %}
|
||||
<a href="{{ path('chill_task_task_transition', { 'taskId': task.id, 'transition': transition.name, 'kind': 'single-task', 'list_params': app.request.query.all }) }}" class="{{ task_workflow_metadata(task, 'transition.class', transition)|e('html_attr') }}">{{ task_workflow_metadata(task, 'transition.verb', transition)|trans }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_task_single_task_show', { 'id': task.id, 'list_params': app.request.query.all }) }}" class="sc-button bt-show "></a>
|
||||
</li>
|
||||
{% if is_granted('CHILL_TASK_TASK_UPDATE', task) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_task_single_task_edit', { 'id': task.id, 'list_params': app.request.query.all }) }}" class="sc-button bt-update "></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_TASK_TASK_DELETE', task) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_task_single_task_delete', { 'id': task.id, 'list_params': app.request.query.all } ) }}" class="sc-button bt-delete "></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if isSingleStatus %}
|
||||
{% if tasks|length < paginator.getTotalItems %}
|
||||
{{ chill_pagination(paginator) }}
|
||||
{% endif %}
|
||||
|
||||
<!-- lien retour -->
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
{% if person is not null %}
|
||||
<a href="{{ path('chill_task_singletask_list', {'person_id': person.id}) }}" class="sc-button bt-cancel">
|
||||
{{ 'Back to the list' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user is not null %}
|
||||
<a href="{{ path('chill_task_singletask_list') }}" class="sc-button bt-cancel">
|
||||
{{ 'Back to the list' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
{% if person is not null %}
|
||||
<a href="{{ path('chill_task_single_task_new', {'person_id': person.id}) }}" class="sc-button bt-create">
|
||||
{{ 'Add a new task' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user is not null %}
|
||||
<a href="{{ path('chill_task_single_task_new') }}" class="sc-button bt-create">
|
||||
{{ 'Add a new task' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a href="{{ path('chill_task_singletask_list', app.request.query.all|merge({ 'status': [ status ] })) }}" class="sc-button">
|
||||
{{ 'See more' | trans }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% import _self as helper %}
|
||||
|
||||
|
||||
<h1>{{ app.request.query.get('title', null)|escape('html')|default('Task list'|trans) }}</h1>
|
||||
|
||||
{% if false == app.request.query.boolean('hide_form', false) %}
|
||||
<h2>{{ 'Filter the tasks'|trans }}</h2>
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.user_id) }}
|
||||
|
||||
{% if form.status is defined %}
|
||||
{{ form_row(form.status) }}
|
||||
{% endif %}
|
||||
|
||||
{% if form.types is defined %}
|
||||
{{ form_row(form.types) }}
|
||||
{% endif %}
|
||||
|
||||
{% if form.person_id is defined %}
|
||||
{{ form_row(form.person_id) }}
|
||||
{% endif %}
|
||||
|
||||
{% if form.center_id is defined %}
|
||||
{{ form_row(form.center_id) }}
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="submit" class="sc-button">{{ 'Filter'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
{{ form_end(form)}}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% if tasks_count == 0 %}
|
||||
<p class="chill-no-data-statement">{{ "There is no tasks."|trans }}</p>
|
||||
{% if person is not null and is_granted('CHILL_TASK_TASK_CREATE', person) %}
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
{% if person is not null %}
|
||||
<a href="{{ path('chill_task_single_task_new', {'person_id': person.id}) }}" class="sc-button bt-create">
|
||||
{{ 'Add a new task' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
||||
{% if false == app.request.query.boolean('hide_form', false) %}
|
||||
<h2>{{ 'Tasks'|trans }}</h2>
|
||||
{% endif %}
|
||||
|
||||
{% if person is not null and is_granted('CHILL_TASK_TASK_CREATE', person) %}
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
{% if person is not null %}
|
||||
<a href="{{ path('chill_task_single_task_new', {'person_id': person.id}) }}" class="sc-button bt-create">
|
||||
{{ 'Add a new task' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if single_task_ended_tasks is defined %}
|
||||
{{ helper.date_status('Tasks with expired deadline', single_task_ended_tasks, single_task_ended_count, single_task_ended_paginator, 'ended', isSingleStatus, person) }}
|
||||
{% endif %}
|
||||
|
||||
{% if single_task_warning_tasks is defined %}
|
||||
{{ helper.date_status('Tasks with warning deadline reached', single_task_warning_tasks, single_task_warning_count, single_task_warning_paginator, 'warning', isSingleStatus, person) }}
|
||||
{% endif %}
|
||||
|
||||
{% if single_task_current_tasks is defined %}
|
||||
{{ helper.date_status('Current tasks', single_task_current_tasks, single_task_current_count, single_task_current_paginator, 'current', isSingleStatus, person) }}
|
||||
{% endif %}
|
||||
|
||||
{% if single_task_not_started_tasks is defined %}
|
||||
{{ helper.date_status('Tasks not started', single_task_not_started_tasks, single_task_not_started_count, single_task_not_started_paginator, 'not_started', isSingleStatus, person) }}
|
||||
{% endif %}
|
||||
|
||||
{% if single_task_closed_tasks is defined %}
|
||||
{{ helper.date_status('Closed tasks', single_task_closed_tasks, single_task_closed_count, single_task_closed_paginator, 'closed', isSingleStatus, person) }}
|
||||
{% endif %}
|
||||
|
||||
{% if isSingleStatus == false %}
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
{% if person is not null and is_granted('CHILL_TASK_TASK_CREATE', person) %}
|
||||
<a href="{{ path('chill_task_single_task_new', {'person_id': person.id}) }}" class="sc-button bt-create">
|
||||
{{ 'Add a new task' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
@ -0,0 +1,19 @@
|
||||
{% extends "ChillPersonBundle::layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_task_task_list' %}
|
||||
{% set person = task.person %}
|
||||
|
||||
{% block title 'Remove task'|trans %}
|
||||
|
||||
{% block personcontent %}
|
||||
|
||||
{{ include('ChillMainBundle:Util:confirmation_template.html.twig',
|
||||
{
|
||||
'title' : 'Remove task'|trans,
|
||||
'confirm_question' : 'Are you sure you want to remove the task about "%name%" ?'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname } ),
|
||||
'cancel_route' : 'chill_task_singletask_list',
|
||||
'cancel_parameters' : app.request.query.get('list_params', { } ),
|
||||
'form' : delete_form
|
||||
} ) }}
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,54 @@
|
||||
{#
|
||||
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
{% extends "ChillPersonBundle::layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_task_single_task_edit' %}
|
||||
{% set person = task.person %}
|
||||
|
||||
{% block title %}{{ 'Edit task'|trans }}{% endblock %}
|
||||
|
||||
{% block personcontent %}
|
||||
<h1 class="chill-red">{{ 'Edit task'|trans }}</h1>
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
{{ form_row(form.title) }}
|
||||
{{ form_row(form.description) }}
|
||||
{{ form_row(form.assignee) }}
|
||||
{{ form_row(form.circle) }}
|
||||
{{ form_row(form.startDate) }}
|
||||
{{ form_row(form.endDate) }}
|
||||
{{ form_row(form.warningInterval) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a class="sc-button bt-cancel" href="{% if app.request.query.has('returnPath') %}
|
||||
{{ app.request.query.get('returnPath')|escape('html_attr') }}">
|
||||
{% else %}
|
||||
{{ path('chill_task_single_task_show', { 'id': task.id, 'list_params': app.request.query.get('list_params', { } )} ) }}" class="sc-button">
|
||||
{% endif %}
|
||||
{{ app.request.query.get('returnLabel')|default('Cancel'|trans) }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ form_widget(form.submit, { 'label': 'Save task', 'attr': {'class' : 'sc-button bt-update'}})}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,45 @@
|
||||
{#
|
||||
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
|
||||
{% extends layout %}
|
||||
|
||||
{% set activeRouteKey = 'chill_task_single_task_new' %}
|
||||
|
||||
{% block title %}{{ 'Task list'|trans }}{% endblock %}
|
||||
|
||||
{% macro thead() %}
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
{% macro row(task) %}
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{# filter tasks #}
|
||||
|
||||
{% if person is not null %}
|
||||
{% block personcontent %}
|
||||
{% include 'ChillTaskBundle:SingleTask:_list.html.twig' %}
|
||||
{% endblock %}
|
||||
{% else %}
|
||||
{% block content %}
|
||||
<div class="grid-8 push-1 grid-mobile-9 grid-tablet-9 push-mobile-0 push-tablet-0">
|
||||
{% include 'ChillTaskBundle:SingleTask:_list.html.twig' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endif %}
|
@ -0,0 +1,47 @@
|
||||
{#
|
||||
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
{% extends "ChillPersonBundle::layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_task_single_task_new' %}
|
||||
{% set person = task.person %}
|
||||
|
||||
{% block title %}{{ 'New task'|trans }}{% endblock %}
|
||||
|
||||
{% block personcontent %}
|
||||
<h1 class="chill-red">{{ 'New task'|trans }}</h1>
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
{{ form_errors(form) }}
|
||||
|
||||
{{ form_row(form.title) }}
|
||||
{{ form_row(form.description) }}
|
||||
{{ form_row(form.assignee) }}
|
||||
{{ form_row(form.circle) }}
|
||||
{{ form_row(form.startDate) }}
|
||||
{{ form_row(form.endDate) }}
|
||||
{{ form_row(form.warningInterval) }}
|
||||
|
||||
<div class="grid-12 centered sticky-form-buttons">
|
||||
<!-- {{ form_row(form.submit, { 'label': 'Add a new task'|trans, 'attr': {'class': 'fa fa-save sc-button green margin-10'} }) }} -->
|
||||
|
||||
<button class="sc-button green margin-10" type="submit"><i class="fa fa-save"></i> {{ 'Add a new task'|trans }}</button>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
125
src/Bundle/ChillTask/Resources/views/SingleTask/show.html.twig
Normal file
125
src/Bundle/ChillTask/Resources/views/SingleTask/show.html.twig
Normal file
@ -0,0 +1,125 @@
|
||||
{#
|
||||
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
{% extends "ChillPersonBundle::layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_task_single_task_show' %}
|
||||
{% set person = task.person %}
|
||||
|
||||
{% block title %}{{ 'Task'|trans }}{% endblock %}
|
||||
|
||||
|
||||
{% block personcontent %}
|
||||
<h1 class="chill-red">{{ 'Task'|trans }}</h1>
|
||||
|
||||
<h2>{{ task.title }} {% for place in workflow_marked_places(task) %}
|
||||
<span class="task-status box type-{{ task.type }} place-{{ place }}">{{ place|trans }}</span>
|
||||
{% endfor %}</h2>
|
||||
|
||||
<dl class="chill_view_data">
|
||||
|
||||
<dt class="inline">{{ 'Description'|trans }}</dt>
|
||||
<dd>
|
||||
{% if task.description is empty %}
|
||||
<span class="chill-no-data-statement">{{"No description"|trans}}</span>
|
||||
{% else %}
|
||||
<blockquote class="chill-user-quote">
|
||||
{{ task.description }}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="inline">{{ 'Assignee'|trans }}</dt>
|
||||
<dd>
|
||||
{% if task.assignee is null %}
|
||||
<span class="chill-no-data-statement">{{"No one assignee"|trans}}</span>
|
||||
{% else %}
|
||||
{{ task.assignee }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="inline">{{ 'Scope'|trans }}</dt>
|
||||
<dd><span class="scope">{{ task.scope.name|localize_translatable_string }}</span></dd>
|
||||
|
||||
<h3>{{"Dates"|trans}}</h3>
|
||||
{% if task.startDate is null and task.endDate is null and task.warningDate is null %}
|
||||
<dt>
|
||||
<dd><span class="chill-no-data-statement">{{"No dates specified"|trans}}</span></dd>
|
||||
</dt>
|
||||
{% else %}
|
||||
{% if task.startDate is not null %}
|
||||
<dt class="inline">{{ 'Start'|trans }}</dt>
|
||||
<dd>{{ task.startDate|localizeddate('long', 'none') }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if task.endDate is not null %}
|
||||
<dt class="inline">{{ 'End'|trans }}</dt>
|
||||
<dd>{{ task.endDate|localizeddate('long', 'none') }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if task.warningDate is not null %}
|
||||
<dt class="inline">{{ 'Warning'|trans }}</dt>
|
||||
<dd>{{ task.warningDate|localizeddate('long', 'none') }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
{% if timeline is not null %}
|
||||
<h3>{{"Timeline"|trans}}</h3>
|
||||
{{ timeline|raw }}
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li class="cancel">
|
||||
<a class="sc-button bt-cancel" href="{%- if app.request.query.has('returnPath') -%}
|
||||
{{ app.request.query.get('returnPath')|escape('html_attr') }}
|
||||
{%- else -%}
|
||||
{{ path('chill_task_singletask_list', app.request.query.get('list_params', {}) ) }}
|
||||
{%- endif -%}">
|
||||
{{ app.request.query.get('returnLabel')|default('Back to the list'|trans) }}
|
||||
</a>
|
||||
</li>
|
||||
{% if workflow_transitions(task)|length > 0 %}
|
||||
<li>
|
||||
<div class="bt-dropdown">
|
||||
<a href="" class="sc-button bt-task-exchange"> {{'Change task status'|trans}}</a>
|
||||
<div class="bt-dropdown-content">
|
||||
{% for transition in workflow_transitions(task) %}
|
||||
<a href="{{ path('chill_task_task_transition', { 'taskId': task.id, 'transition': transition.name, 'kind': 'single-task', 'return_path': app.request.uri }) }}" class="{{ task_workflow_metadata(task, 'transition.class', transition)|e('html_attr') }}">{{ task_workflow_metadata(task, 'transition.verb', transition)|trans }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_TASK_TASK_UPDATE', task) %}
|
||||
<li>
|
||||
<a class="sc-button bt-update" href="{{ path('chill_task_single_task_edit', { 'id': task.id }) }}">
|
||||
{{ 'Edit the task'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_TASK_TASK_CREATE', task) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_task_single_task_delete', { 'id': task.id } ) }}" class="sc-button bt-delete">
|
||||
{{ 'Delete'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,34 @@
|
||||
{% extends "ChillPersonBundle::layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_task_task_list' %}
|
||||
{% set person = task.person %}
|
||||
|
||||
{% block title 'Remove task'|trans %}
|
||||
|
||||
{% block personcontent %}
|
||||
|
||||
<h2>{{ 'Apply transition on task <em>%title%</em>'|trans({ '%title%': task.title } )|raw }}</h2>
|
||||
|
||||
|
||||
{% if task_workflow_metadata(task, 'transition.sentence_confirmation', transition) is not empty %}
|
||||
<p class="message-confirm">{{ task_workflow_metadata(task, 'transition.sentence_confirmation', transition)|trans }}</p>
|
||||
{% else %}
|
||||
<p>{{ 'Are you sure to apply the transition %name% on this task ?'|trans({ '%name%': task_workflow_metadata(task, 'transition.name', transition)|default(transition.name)|trans }) }}</p>
|
||||
{% endif %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('chill_task_singletask_list', app.request.query.get('list_params', { }) ) }}" class="sc-button bt-cancel">
|
||||
{{ 'Back to the list'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ form_widget(form.submit, { 'attr' : { 'class' : "sc-button bt-task-exchange green" }, 'label': task_workflow_metadata(task, 'transition.apply_transition_submit_label', transition)|default('apply')|trans } ) }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,33 @@
|
||||
<div>
|
||||
<h3 class="single-line">
|
||||
{{ event.datetime|localizeddate('long', 'none') }}
|
||||
<span class="task"> / {{ 'Task'|trans }}</span> /
|
||||
{% if transition is not null %}
|
||||
<span class="statement">{{ task_workflow_metadata(event.task, 'transition.sentence', transition)|trans({ '%user%': event.author.username }) }}</span>
|
||||
{% else %}
|
||||
<span class="statement">{{ '%user% has created the task'|trans({ '%user%': event.author.username }) }}</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
<div class="statement">
|
||||
<dl class="chill_view_data">
|
||||
<dt class="inline">{{ 'Title'|trans }}</dt>
|
||||
<dd>{{ event.task.title }}</dd>
|
||||
|
||||
{% if event.task.description is not empty %}
|
||||
<dt class="inline">{{ 'Description'|trans }}</dt>
|
||||
<dd>
|
||||
<blockquote class="chill-user-quote">
|
||||
{{ event.task.description }}
|
||||
</blockquote>
|
||||
</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if event.task.endDate is not empty %}
|
||||
<dt class="inline">{{ 'Task end date'|trans }}</dt>
|
||||
<dd>{{ event.task.endDate|localizeddate('medium', 'none') }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<h3 class="single-line">
|
||||
{{ event.datetime|localizeddate('long', 'short') }}
|
||||
<span class="task">
|
||||
{% if transition is not null %}
|
||||
<span class="statement">{{ task_workflow_metadata(event.task, 'transition.sentence', transition)|trans({ '%user%': event.author.username }) }}</span>
|
||||
{% else %}
|
||||
<span class="statement">{{ '%user% has created the task'|trans({ '%user%': event.author.username }) }}</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/*
|
||||
*
|
||||
*/
|
||||
namespace Chill\TaskBundle\Security\Authorization;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class AuthorizationEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var Chill\TaskBundle\Entity\AbstractTask|\Chill\PersonBundle\Entity\Person|null
|
||||
*/
|
||||
protected $subject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $attribute;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TokenInterface
|
||||
*/
|
||||
protected $token;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $vote;
|
||||
|
||||
const VOTE = 'chill_task.vote';
|
||||
|
||||
public function __construct(
|
||||
$subject,
|
||||
$attribute,
|
||||
TokenInterface $token
|
||||
) {
|
||||
$this->subject = $subject;
|
||||
$this->attribute = $attribute;
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function getSubject()
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
public function getAttribute()
|
||||
{
|
||||
return $this->attribute;
|
||||
}
|
||||
|
||||
public function getToken(): TokenInterface
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function getVote()
|
||||
{
|
||||
return $this->vote;
|
||||
}
|
||||
|
||||
public function setVote($vote)
|
||||
{
|
||||
$this->vote = $vote;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasVote()
|
||||
{
|
||||
return $this->vote !== NULL;
|
||||
}
|
||||
|
||||
public function removeVote()
|
||||
{
|
||||
$this->vote = NULL;
|
||||
}
|
||||
}
|
175
src/Bundle/ChillTask/Security/Authorization/TaskVoter.php
Normal file
175
src/Bundle/ChillTask/Security/Authorization/TaskVoter.php
Normal file
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Chill\TaskBundle\Security\Authorization;
|
||||
|
||||
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
|
||||
use Chill\TaskBundle\Entity\AbstractTask;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Chill\TaskBundle\Security\Authorization\AuthorizationEvent;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class TaskVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
|
||||
{
|
||||
const CREATE = 'CHILL_TASK_TASK_CREATE';
|
||||
const UPDATE = 'CHILL_TASK_TASK_UPDATE';
|
||||
const SHOW = 'CHILL_TASK_TASK_SHOW';
|
||||
const DELETE = 'CHILL_TASK_TASK_DELETE';
|
||||
|
||||
const ROLES = [
|
||||
self::CREATE,
|
||||
self::UPDATE,
|
||||
self::SHOW,
|
||||
self::DELETE
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AccessDecisionManagerInterface
|
||||
*/
|
||||
protected $accessDecisionManager;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
|
||||
public function __construct(
|
||||
AccessDecisionManagerInterface $accessDecisionManager,
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->accessDecisionManager = $accessDecisionManager;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function supports($attribute, $subject)
|
||||
{
|
||||
return ($subject instanceof AbstractTask && in_array($attribute, self::ROLES))
|
||||
||
|
||||
($subject instanceof Person && \in_array($attribute, [ self::CREATE, self::SHOW ]))
|
||||
||
|
||||
(NULL === $subject && $attribute === self::SHOW )
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param AbstractTask $subject
|
||||
* @param TokenInterface $token
|
||||
* @return boolean
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
$this->logger->debug(sprintf("Voting from %s class", self::class));
|
||||
|
||||
if (!$token->getUser() instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$event = new AuthorizationEvent($subject, $attribute, $token);
|
||||
|
||||
$this->eventDispatcher->dispatch(AuthorizationEvent::VOTE, $event);
|
||||
|
||||
if ($event->hasVote()) {
|
||||
|
||||
$this->logger->debug("The TaskVoter is overriding by "
|
||||
.AuthorizationEvent::VOTE, [
|
||||
'vote' => $event->getVote(),
|
||||
'task_id' => $subject->getId()
|
||||
]);
|
||||
|
||||
return $event->getVote();
|
||||
}
|
||||
|
||||
if ($subject instanceof AbstractTask) {
|
||||
if ($subject->getPerson() === null) {
|
||||
throw new \LogicException("You should associate a person with task "
|
||||
. "in order to check autorizations");
|
||||
}
|
||||
|
||||
$person = $subject->getPerson();
|
||||
} elseif ($subject instanceof Person) {
|
||||
$person = $subject;
|
||||
} else {
|
||||
// subject is null. We check that at least one center is reachable
|
||||
$centers = $this->authorizationHelper->getReachableCenters($token->getUser(), new Role($attribute));
|
||||
|
||||
return count($centers) > 0;
|
||||
}
|
||||
|
||||
if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->authorizationHelper->userHasAccess(
|
||||
$token->getUser(),
|
||||
$subject,
|
||||
$attribute
|
||||
);
|
||||
}
|
||||
|
||||
public function getRoles()
|
||||
{
|
||||
return self::ROLES;
|
||||
}
|
||||
|
||||
public function getRolesWithHierarchy(): array
|
||||
{
|
||||
return [
|
||||
'Task' => self::ROLES
|
||||
];
|
||||
}
|
||||
|
||||
public function getRolesWithoutScope()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
59
src/Bundle/ChillTask/Templating/TaskTwigExtension.php
Normal file
59
src/Bundle/ChillTask/Templating/TaskTwigExtension.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Templating;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
use Chill\TaskBundle\Entity\AbstractTask;
|
||||
use Chill\TaskBundle\Workflow\TaskWorkflowManager;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class TaskTwigExtension extends \Twig_Extension
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var TaskWorkflowManager
|
||||
*/
|
||||
protected $taskWorkflowManager;
|
||||
|
||||
public function __construct(TaskWorkflowManager $taskWorkflowManager)
|
||||
{
|
||||
$this->taskWorkflowManager = $taskWorkflowManager;
|
||||
}
|
||||
|
||||
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction('task_workflow_metadata', [ $this, 'getWorkflowMetadata' ] )
|
||||
];
|
||||
}
|
||||
|
||||
public function getWorkflowMetadata(
|
||||
AbstractTask $task,
|
||||
string $key,
|
||||
$metadataSubject = null,
|
||||
string $name = null
|
||||
) {
|
||||
return $this->taskWorkflowManager->getWorkflowMetadata($task, $key, $metadataSubject, $name);
|
||||
}
|
||||
}
|
129
src/Bundle/ChillTask/Templating/UI/CountNotificationTask.php
Normal file
129
src/Bundle/ChillTask/Templating/UI/CountNotificationTask.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Templating\UI;
|
||||
|
||||
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\TaskBundle\Repository\SingleTaskRepository;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\Workflow\Event\Event;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class CountNotificationTask implements NotificationCounterInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var SingleTaskRepository
|
||||
*/
|
||||
protected $singleTaskRepository;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var CacheItempPoolInterface
|
||||
*/
|
||||
protected $cachePool;
|
||||
|
||||
const CACHE_KEY = 'chill_task.count_notifications.user.%d.%s';
|
||||
|
||||
public function __construct(
|
||||
SingleTaskRepository $singleTaskRepository,
|
||||
CacheItemPoolInterface $cachePool
|
||||
) {
|
||||
$this->singleTaskRepository = $singleTaskRepository;
|
||||
$this->cachePool = $cachePool;
|
||||
}
|
||||
|
||||
public function countNotification(UserInterface $u): int
|
||||
{
|
||||
return
|
||||
$this->countNotificationEnded($u)
|
||||
+ $this->countNotificationWarning($u);
|
||||
}
|
||||
|
||||
public function countNotificationEnded(UserInterface $u): int
|
||||
{
|
||||
return $this->_countNotification($u, SingleTaskRepository::DATE_STATUS_ENDED);
|
||||
}
|
||||
|
||||
public function countNotificationWarning(UserInterface $u): int
|
||||
{
|
||||
return $this->_countNotification($u, SingleTaskRepository::DATE_STATUS_WARNING);
|
||||
}
|
||||
|
||||
protected function _countNotification(UserInterface $u, $status)
|
||||
{
|
||||
if (!$u instanceof User) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$sumCache = $this->cachePool->getItem($this->getCacheKey($u, $status));
|
||||
|
||||
if ($sumCache->isHit()) {
|
||||
return $sumCache->get();
|
||||
}
|
||||
|
||||
$params = [
|
||||
'user' => $u,
|
||||
'is_closed' => false
|
||||
];
|
||||
|
||||
$sum = $this->singleTaskRepository->countByParameters(
|
||||
\array_merge($params, [ 'date_status' => $status ])
|
||||
);
|
||||
|
||||
$sumCache->set($sum);
|
||||
$this->cachePool->save($sumCache);
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
public function addNotification(UserInterface $u): int
|
||||
{
|
||||
return $this->countNotification($u);
|
||||
}
|
||||
|
||||
public function resetCacheOnNewStates(Event $e)
|
||||
{
|
||||
/* @var $task \Chill\TaskBundle\Entity\SingleTask */
|
||||
$task = $e->getSubject();
|
||||
|
||||
if (NULL !== $task->getAssignee()) {
|
||||
foreach ([
|
||||
SingleTaskRepository::DATE_STATUS_ENDED,
|
||||
SingleTaskRepository::DATE_STATUS_WARNING
|
||||
] as $status) {
|
||||
$key = $this->getCacheKey($task->getAssignee(), $status);
|
||||
$sumCache = $this->cachePool->getItem($key);
|
||||
|
||||
if ($sumCache->isHit()) {
|
||||
$this->cachePool->deleteItem($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getCacheKey(User $u, $status)
|
||||
{
|
||||
return sprintf(self::CACHE_KEY, $u->getId(), $status);
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Chill\MainBundle\Tests\TestHelper;
|
||||
use Faker;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
|
||||
class SingleTaskControllerTest extends WebTestCase
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var Faker\Generator
|
||||
*/
|
||||
protected $faker;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->faker = Faker\Factory::create('fr');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return \Chill\PersonBundle\Entity\Person
|
||||
*/
|
||||
protected function getRandomPerson($centerName)
|
||||
{
|
||||
$em = self::$kernel
|
||||
->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
;
|
||||
|
||||
$centers = $em
|
||||
->getRepository(Center::class)
|
||||
->findAll();
|
||||
|
||||
$center = \array_filter(
|
||||
$centers,
|
||||
function(Center $c) use ($centerName) {
|
||||
return $centerName === $c->getName();
|
||||
})[0];
|
||||
|
||||
$ids = $em
|
||||
->createQuery('SELECT p.id FROM ChillPersonBundle:Person p '
|
||||
. 'WHERE p.center = :center'
|
||||
)
|
||||
->setParameter('center', $center)
|
||||
->getResult()
|
||||
;
|
||||
|
||||
$id = $ids[\array_rand($ids)];
|
||||
|
||||
return self::$kernel
|
||||
->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
->getRepository(\Chill\PersonBundle\Entity\Person::class)
|
||||
->find($id)
|
||||
;
|
||||
}
|
||||
|
||||
public function testNew()
|
||||
{
|
||||
$client = static::createClient(
|
||||
array(),
|
||||
TestHelper::getAuthenticatedClientOptions()
|
||||
);
|
||||
$person = $this->getRandomPerson('Center A');
|
||||
|
||||
$crawler = $client->request('GET', '/fr/task/single-task/new', [
|
||||
'person_id' => $person->getId()
|
||||
]);
|
||||
var_dump($crawler->text());
|
||||
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
|
||||
|
||||
$form = $crawler->selectButton('Envoi')->form();
|
||||
|
||||
$title = $this->faker->sentence;
|
||||
$circles = $form->get('circle')
|
||||
->availableOptionsValues()
|
||||
;
|
||||
|
||||
$client->submit($form, [
|
||||
'title' => $title,
|
||||
'circle' => $circles[\array_rand($circles)]
|
||||
]);
|
||||
|
||||
$this->assertTrue($client->getResponse()->isRedirect(sprintf(
|
||||
'/fr/task/task/list/%d', $person->getId())));
|
||||
|
||||
$crawler = $client->followRedirect();
|
||||
|
||||
$this->assertContains($title, $crawler->text(),
|
||||
"Assert that newly created task title is shown in list page")
|
||||
;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\TaskBundle\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class TaskControllerTest extends WebTestCase
|
||||
{
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Timeline;
|
||||
|
||||
use Chill\MainBundle\Timeline\TimelineProviderInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Chill\TaskBundle\Entity\Task\SingleTaskPlaceEvent;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\Workflow;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class SingleTaskTaskLifeCycleEventTimelineProvider implements TimelineProviderInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Registry
|
||||
*/
|
||||
protected $registry;
|
||||
|
||||
const TYPE = 'chill_task.transition';
|
||||
|
||||
public function __construct(EntityManagerInterface $em, Registry $registry)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function fetchQuery($context, $args)
|
||||
{
|
||||
if ($context !== 'task') {
|
||||
throw new \LogicException(sprintf('%s is not able '
|
||||
. 'to render context %s', self::class, $context));
|
||||
}
|
||||
|
||||
$metadata = $this->em
|
||||
->getClassMetadata(SingleTaskPlaceEvent::class);
|
||||
$singleTaskMetadata = $this->em
|
||||
->getClassMetadata(SingleTask::class);
|
||||
|
||||
return [
|
||||
'id' => sprintf('%s.%s.%s', $metadata->getSchemaName(), $metadata->getTableName(), $metadata->getColumnName('id')),
|
||||
'type' => self::TYPE,
|
||||
'date' => $metadata->getColumnName('datetime'),
|
||||
'FROM' => sprintf('%s JOIN %s ON %s = %s',
|
||||
sprintf('%s.%s', $metadata->getSchemaName(), $metadata->getTableName()),
|
||||
sprintf('%s.%s', $singleTaskMetadata->getSchemaName(), $singleTaskMetadata->getTableName()),
|
||||
$metadata->getAssociationMapping('task')['joinColumns'][0]['name'],
|
||||
sprintf('%s.%s.%s', $singleTaskMetadata->getSchemaName(), $singleTaskMetadata->getTableName(), $singleTaskMetadata->getColumnName('id'))
|
||||
),
|
||||
'WHERE' => sprintf('%s.%s = %d',
|
||||
sprintf('%s.%s', $singleTaskMetadata->getSchemaName(), $singleTaskMetadata->getTableName()),
|
||||
$singleTaskMetadata->getColumnName('id'),
|
||||
$args['task']->getId()
|
||||
)
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
public function getEntities(array $ids)
|
||||
{
|
||||
$events = $this->em
|
||||
->getRepository(SingleTaskPlaceEvent::class)
|
||||
->findBy([ 'id' => $ids ])
|
||||
;
|
||||
|
||||
return \array_combine(
|
||||
\array_map(function($e) { return $e->getId(); }, $events ),
|
||||
$events
|
||||
);
|
||||
}
|
||||
|
||||
public function getEntityTemplate($entity, $context, array $args)
|
||||
{
|
||||
$workflow = $this->registry->get($entity->getTask(), $entity->getData['workflow']);
|
||||
$transition = $this->getTransitionByName($entity->getTransition(), $workflow);
|
||||
|
||||
return [
|
||||
'template' => 'ChillTaskBundle:Timeline:single_task_transition_task_context.html.twig',
|
||||
'template_data' => [
|
||||
'task' => $args['task'],
|
||||
'event' => $entity,
|
||||
'transition' => $transition
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $name
|
||||
* @param Workflow $workflow
|
||||
* @return \Symfony\Component\Workflow\Transition
|
||||
*/
|
||||
protected function getTransitionByName($name, Workflow $workflow)
|
||||
{
|
||||
foreach ($workflow->getDefinition()->getTransitions() as $transition) {
|
||||
if ($transition->getName() === $name) {
|
||||
return $transition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function supportsType($type): bool
|
||||
{
|
||||
return $type === self::TYPE;
|
||||
}
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Timeline;
|
||||
|
||||
use Chill\MainBundle\Timeline\TimelineProviderInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Chill\TaskBundle\Entity\Task\SingleTaskPlaceEvent;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\Workflow;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class TaskLifeCycleEventTimelineProvider implements TimelineProviderInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Registry
|
||||
*/
|
||||
protected $registry;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $tokenStorage;
|
||||
|
||||
const TYPE = 'chill_task.transition';
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
Registry $registry,
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
TokenStorageInterface $tokenStorage
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->registry = $registry;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
}
|
||||
|
||||
public function fetchQuery($context, $args)
|
||||
{
|
||||
if ($context !== 'person') {
|
||||
throw new \LogicException(sprintf('%s is not able '
|
||||
. 'to render context %s', self::class, $context));
|
||||
}
|
||||
|
||||
$metadata = $this->em
|
||||
->getClassMetadata(SingleTaskPlaceEvent::class);
|
||||
$singleTaskMetadata = $this->em
|
||||
->getClassMetadata(SingleTask::class);
|
||||
$user = $this->tokenStorage->getToken()->getUser();
|
||||
$circles = $this->authorizationHelper->getReachableCircles(
|
||||
$user, new Role(ActivityVoter::SEE_DETAILS), $args['person']->getCenter());
|
||||
|
||||
|
||||
if (count($circles) > 0) {
|
||||
$circlesId = \array_map(function($c) { return $c->getId(); }, $circles);
|
||||
$circleRestriction = sprintf('%s.%s.%s IN (%s)',
|
||||
$singleTaskMetadata->getSchemaName(), // chill_task schema
|
||||
$singleTaskMetadata->getTableName(), // single_task table name
|
||||
$singleTaskMetadata->getAssociationMapping('circle')['joinColumns'][0]['name'],
|
||||
\implode(', ', $circlesId)
|
||||
);
|
||||
} else {
|
||||
$circleRestriction = 'FALSE = TRUE';
|
||||
}
|
||||
|
||||
|
||||
return [
|
||||
'id' => sprintf('%s.%s.%s', $metadata->getSchemaName(), $metadata->getTableName(), $metadata->getColumnName('id')),
|
||||
'type' => self::TYPE,
|
||||
'date' => $metadata->getColumnName('datetime'),
|
||||
'FROM' => sprintf('%s JOIN %s ON %s = %s',
|
||||
sprintf('%s.%s', $metadata->getSchemaName(), $metadata->getTableName()),
|
||||
sprintf('%s.%s', $singleTaskMetadata->getSchemaName(), $singleTaskMetadata->getTableName()),
|
||||
$metadata->getAssociationMapping('task')['joinColumns'][0]['name'],
|
||||
sprintf('%s.%s.%s', $singleTaskMetadata->getSchemaName(), $singleTaskMetadata->getTableName(), $singleTaskMetadata->getColumnName('id'))
|
||||
),
|
||||
'WHERE' => sprintf('%s.%s = %d and %s',
|
||||
sprintf('%s.%s', $singleTaskMetadata->getSchemaName(), $singleTaskMetadata->getTableName()),
|
||||
$singleTaskMetadata->getAssociationMapping('person')['joinColumns'][0]['name'],
|
||||
$args['person']->getId(),
|
||||
$circleRestriction
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
public function getEntities(array $ids)
|
||||
{
|
||||
$events = $this->em
|
||||
->getRepository(SingleTaskPlaceEvent::class)
|
||||
->findBy([ 'id' => $ids ])
|
||||
;
|
||||
|
||||
return \array_combine(
|
||||
\array_map(function($e) { return $e->getId(); }, $events ),
|
||||
$events
|
||||
);
|
||||
}
|
||||
|
||||
public function getEntityTemplate($entity, $context, array $args)
|
||||
{
|
||||
$workflow = $this->registry->get($entity->getTask(), $entity->getData['workflow']);
|
||||
$transition = $this->getTransitionByName($entity->getTransition(), $workflow);
|
||||
|
||||
return [
|
||||
'template' => 'ChillTaskBundle:Timeline:single_task_transition_person_context.html.twig',
|
||||
'template_data' => [
|
||||
'person' => $args['person'],
|
||||
'event' => $entity,
|
||||
'transition' => $transition
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $name
|
||||
* @param Workflow $workflow
|
||||
* @return \Symfony\Component\Workflow\Transition
|
||||
*/
|
||||
protected function getTransitionByName($name, Workflow $workflow)
|
||||
{
|
||||
foreach ($workflow->getDefinition()->getTransitions() as $transition) {
|
||||
if ($transition->getName() === $name) {
|
||||
return $transition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function supportsType($type): bool
|
||||
{
|
||||
return $type === self::TYPE;
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Workflow\Definition;
|
||||
|
||||
use Chill\TaskBundle\Entity\AbstractTask;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class DefaultTaskDefinition implements \Chill\TaskBundle\Workflow\TaskWorkflowDefinition
|
||||
{
|
||||
const TRANSITION_METADATA = [
|
||||
'close' => [
|
||||
'verb' => 'close',
|
||||
'class' => 'sc-button bt-task-label bt-task-close',
|
||||
'sentence' => '%user% has closed the task',
|
||||
'sentence_confirmation' => 'Are you sure you want to close this task ?',
|
||||
'apply_transition_submit_label' => 'Close_verb'
|
||||
],
|
||||
'cancel' => [
|
||||
'verb' => 'cancel',
|
||||
'class' => 'sc-button bt-task-label bt-task-cancel',
|
||||
'sentence' => '%user% has canceled the task',
|
||||
'sentence_confirmation' => 'Are you sure you want to cancel this task ?',
|
||||
'apply_transition_submit_label' => 'Set this task to cancel state'
|
||||
],
|
||||
'start' => [
|
||||
'verb' => 'start',
|
||||
'class' => 'sc-button bt-task-label bt-task-start',
|
||||
'sentence' => '%user% has started the task',
|
||||
'sentence_confirmation' => 'Are you sure you want to start this task ?',
|
||||
'apply_transition_submit_label' => 'Start_verb'
|
||||
]
|
||||
];
|
||||
|
||||
const DEFINITION_METADATA = [
|
||||
'name' => 'Default task'
|
||||
];
|
||||
|
||||
public function supports(AbstractTask $task)
|
||||
{
|
||||
|
||||
return $task instanceof SingleTask
|
||||
&& $task->getType() === 'task_default';
|
||||
}
|
||||
|
||||
public static function getAssociatedWorkflowName()
|
||||
{
|
||||
return 'task_default';
|
||||
}
|
||||
|
||||
public function getWorkflowMetadata(
|
||||
AbstractTask $task,
|
||||
string $key,
|
||||
$metadataSubject = null
|
||||
) {
|
||||
$keys = \explode('.', $key);
|
||||
|
||||
switch($keys[0]) {
|
||||
case 'transition':
|
||||
if (!$metadataSubject instanceof Transition) {
|
||||
throw new \LogicException("You must give a transition as metadatasubject");
|
||||
}
|
||||
|
||||
return $this->getTransitionMetadata(\implode('.', \array_slice($keys, 1)), $metadataSubject);
|
||||
case 'definition':
|
||||
return self::DEFINITION_METADATA[$keys[1]] ?? $key;
|
||||
default:
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getTransitionMetadata($key, Transition $transition)
|
||||
{
|
||||
if (!\array_key_exists($transition->getName(), self::TRANSITION_METADATA)) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
if (!\array_key_exists($key, self::TRANSITION_METADATA[$transition->getName()])) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
return self::TRANSITION_METADATA[$transition->getName()][$key];
|
||||
}
|
||||
|
||||
public function isClosed(AbstractTask $task)
|
||||
{
|
||||
return \array_key_exists('closed', $task->getCurrentStates())
|
||||
|| \array_key_exists('canceled', $task->getCurrentStates());
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Workflow\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class DefaultTaskGuardEvent implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'workflow.task_default.guard' => [
|
||||
'checkACL'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
protected $authorizationChecker;
|
||||
|
||||
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
|
||||
{
|
||||
$this->authorizationChecker = $authorizationChecker;
|
||||
}
|
||||
|
||||
public function checkACL(GuardEvent $event)
|
||||
{
|
||||
if (FALSE === $this->authorizationChecker->isGranted(TaskVoter::UPDATE,
|
||||
$event->getSubject())) {
|
||||
$event->setBlocked(true);
|
||||
}
|
||||
}
|
||||
}
|
27
src/Bundle/ChillTask/Workflow/TaskWorkflowDefinition.php
Normal file
27
src/Bundle/ChillTask/Workflow/TaskWorkflowDefinition.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Workflow;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
interface TaskWorkflowDefinition
|
||||
{
|
||||
|
||||
}
|
93
src/Bundle/ChillTask/Workflow/TaskWorkflowManager.php
Normal file
93
src/Bundle/ChillTask/Workflow/TaskWorkflowManager.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\TaskBundle\Workflow;
|
||||
|
||||
use Chill\TaskBundle\Entity\AbstractTask;
|
||||
use Symfony\Component\Workflow\SupportStrategy\SupportStrategyInterface;
|
||||
use Symfony\Component\Workflow\Workflow;
|
||||
use Symfony\Component\Workflow\Event\Event;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class TaskWorkflowManager implements SupportStrategyInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var TaskWorkflowDefinition[]
|
||||
*/
|
||||
protected $definitions = array();
|
||||
|
||||
public function addDefinition(TaskWorkflowDefinition $definition) {
|
||||
$this->definitions[] = $definition;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param AbstractTask $task
|
||||
* @return TaskWorkflowDefinition
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function getTaskWorkflowDefinition(AbstractTask $task)
|
||||
{
|
||||
$definitions = array();
|
||||
|
||||
foreach($this->definitions as $tested) {
|
||||
if ($tested->supports($task)) {
|
||||
$definitions[] = $tested;
|
||||
}
|
||||
}
|
||||
|
||||
$count = count($definitions);
|
||||
if ($count > 1) {
|
||||
throw new \LogicException("More than one TaskWorkflowDefinition supports "
|
||||
. "this task. This should not happens.");
|
||||
} elseif ($count === 0) {
|
||||
throw new \LogicException(\sprintf("No taskWorkflowDefinition supports this task type: %s (task id: %s).", $task->getType(), $task->getId()));
|
||||
}
|
||||
|
||||
return $definitions[0];
|
||||
}
|
||||
|
||||
public function supports(Workflow $workflow, $subject): bool
|
||||
{
|
||||
if (!$subject instanceof AbstractTask) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $workflow->getName() === $this
|
||||
->getTaskWorkflowDefinition($subject)->getAssociatedWorkflowName();
|
||||
}
|
||||
|
||||
public function getWorkflowMetadata(AbstractTask $task, string $key, $metadataSubject = null, string $name = null)
|
||||
{
|
||||
return $this->getTaskWorkflowDefinition($task)
|
||||
->getWorkflowMetadata($task, $key, $metadataSubject);
|
||||
}
|
||||
|
||||
public function onTaskStateEntered(Event $e)
|
||||
{
|
||||
$task = $e->getSubject();
|
||||
|
||||
$definition = $this->getTaskWorkflowDefinition($task);
|
||||
|
||||
$task->setClosed($definition->isClosed($task));
|
||||
}
|
||||
}
|
3
src/Bundle/ChillTask/chill.webpack.config.js
Normal file
3
src/Bundle/ChillTask/chill.webpack.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = function(encore, entries) {
|
||||
entries.push(__dirname + '/Resources/public/index.js');
|
||||
};
|
41
src/Bundle/ChillTask/composer.json
Normal file
41
src/Bundle/ChillTask/composer.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "chill-project/task",
|
||||
"description": "Associate task with peoples in chill.social",
|
||||
"type": "symfony-bundle",
|
||||
"keywords": ["chill", "social work", "tasks"],
|
||||
"homepage": "https://framagit.org/Chill-project/Chill-Task",
|
||||
"autoload": {
|
||||
"psr-4": { "Chill\\TaskBundle\\" : "" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [ "Resources/test/Fixtures/App/app/AppKernel.php" ]
|
||||
},
|
||||
"require": {
|
||||
"chill-project/person": "~1.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "^1.7",
|
||||
"phpunit/phpunit": "^7.1"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Champs-Libres",
|
||||
"email": "info@champs-libres.coop"
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
"ComposerBundleMigration\\Composer\\Migrations::synchronizeMigrations",
|
||||
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"ComposerBundleMigration\\Composer\\Migrations::synchronizeMigrations",
|
||||
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"app-migrations-dir": "Resources/test/Fixtures/App/app/DoctrineMigrations",
|
||||
"symfony-app-dir": "Resources/test/Fixtures/App/"
|
||||
}
|
||||
}
|
4403
src/Bundle/ChillTask/composer.lock
generated
Normal file
4403
src/Bundle/ChillTask/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user