717 lines
23 KiB
PHP

<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TaskBundle\Controller;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
use Chill\MainBundle\Timeline\TimelineBuilder;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Privacy\PrivacyEvent;
use Chill\TaskBundle\Entity\SingleTask;
use Chill\TaskBundle\Event\TaskEvent;
use Chill\TaskBundle\Event\UI\UIEvent;
use Chill\TaskBundle\Form\SingleTaskType;
use Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface;
use Chill\TaskBundle\Security\Authorization\TaskVoter;
use LogicException;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Contracts\Translation\TranslatorInterface;
use function array_map;
use function array_merge;
final class SingleTaskController extends AbstractController
{
private CenterResolverDispatcherInterface $centerResolverDispatcher;
private EventDispatcherInterface $eventDispatcher;
private FilterOrderHelperFactoryInterface $filterOrderHelperFactory;
private LoggerInterface $logger;
private PaginatorFactory $paginatorFactory;
private SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository;
private TimelineBuilder $timelineBuilder;
private TranslatorInterface $translator;
public function __construct(
CenterResolverDispatcherInterface $centerResolverDispatcher,
PaginatorFactory $paginatorFactory,
SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository,
TranslatorInterface $translator,
EventDispatcherInterface $eventDispatcher,
TimelineBuilder $timelineBuilder,
LoggerInterface $logger,
FilterOrderHelperFactoryInterface $filterOrderHelperFactory
) {
$this->eventDispatcher = $eventDispatcher;
$this->timelineBuilder = $timelineBuilder;
$this->logger = $logger;
$this->translator = $translator;
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->paginatorFactory = $paginatorFactory;
$this->singleTaskAclAwareRepository = $singleTaskAclAwareRepository;
$this->filterOrderHelperFactory = $filterOrderHelperFactory;
}
/**
* @Route(
* "/{_locale}/task/single-task/{id}/delete",
* name="chill_task_single_task_delete"
* )
*
* @param mixed $id
*/
public function deleteAction(Request $request, $id)
{
$course = null;
$em = $this->getDoctrine()->getManager();
$task = $em->getRepository(SingleTask::class)->find($id);
if (null === $task) {
throw $this->createNotFoundException('Unable to find Task entity.');
}
if ($task->getPerson() !== null) {
$personId = $task->getPerson()->getId();
if (null === $personId) {
return new Response('You must provide a person_id', Response::HTTP_BAD_REQUEST);
}
$person = $this->getDoctrine()->getManager()
->getRepository(Person::class)
->find($personId);
if (null === $person) {
throw $this->createNotFoundException('Invalid person id');
}
} else {
$courseId = $task->getCourse()->getId();
if (null === $courseId) {
return new Response('You must provide a course_id', Response::HTTP_BAD_REQUEST);
}
$course = $this->getDoctrine()->getManager()
->getRepository(AccompanyingPeriod::class)
->find($courseId);
if (null === $course) {
throw $this->createNotFoundException('Invalid accompanying period id');
}
}
// TODO: reactivate right to delete
// $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()) {
$this->logger->notice('A task has been removed', [
'by_user' => $this->getUser()->getUsername(),
'task_id' => $task->getId(),
'description' => $task->getDescription(),
'assignee' => $task->getAssignee(),
// TODO reimplement scope
// 'scope_id' => $task->getScope()->getId(),
]);
$em = $this->getDoctrine()->getManager();
$em->remove($task);
$em->flush();
$this->addFlash('success', $this->translator
->trans('The task has been successfully removed.'));
if ($task->getContext() instanceof Person) {
return $this->redirect($this->generateUrl(
'chill_task_singletask_by-person_list',
['id' => $task->getPerson()->getId()]
));
}
return $this->redirect($this->generateUrl(
'chill_task_singletask_by-course_list',
['id' => $task->getCourse()->getId()]
));
}
}
if ($task->getContext() instanceof Person) {
return $this->render(
'@ChillTask/SingleTask/Person/confirm_delete.html.twig',
[
'task' => $task,
'delete_form' => $form->createView(),
]
);
}
return $this->render(
'@ChillTask/SingleTask/AccompanyingCourse/confirm_delete.html.twig',
[
'task' => $task,
'delete_form' => $form->createView(),
'accompanyingCourse' => $course,
]
);
}
/**
* @Route(
* "/{_locale}/task/single-task/{id}/edit",
* name="chill_task_single_task_edit"
* )
*/
public function editAction(
SingleTask $task,
Request $request
) {
$this->denyAccessUnlessGranted(TaskVoter::UPDATE, $task, 'You are not '
. 'allowed to edit this task');
$event = (new UIEvent('single-task', $task))
->setForm($this->setCreateForm($task, 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', $this->translator
->trans('The task has been updated'));
if ($task->getContext() instanceof Person) {
$event = new PrivacyEvent($task->getPerson(), [
'element_class' => SingleTask::class,
'element_id' => $task->getId(),
'action' => 'update',
]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
if ($request->query->has('returnPath')) {
return $this->redirect($request->query->get('returnPath'));
}
return $this->redirectToRoute(
'chill_task_singletask_list',
);
}
if ($request->query->has('returnPath')) {
return $this->redirect($request->query->get('returnPath'));
}
return $this->redirectToRoute(
'chill_task_singletask_by-course_list',
['id' => $task->getCourse()->getId()]
);
}
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
$this->eventDispatcher->dispatch(UIEvent::EDIT_PAGE, $event);
if ($event->hasResponse()) {
return $event->getResponse();
}
if ($task->getContext() instanceof Person) {
$event = new PrivacyEvent($task->getPerson(), [
'element_class' => SingleTask::class,
'element_id' => $task->getId(),
'action' => 'edit',
]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
return $this->render('@ChillTask/SingleTask/Person/edit.html.twig', [
'task' => $task,
'form' => $form->createView(),
]);
}
return $this->render('@ChillTask/SingleTask/AccompanyingCourse/edit.html.twig', [
'task' => $task,
'form' => $form->createView(),
'accompanyingCourse' => $task->getCourse(),
]);
}
/**
* Arguments:
* - user_id
* - scope_id
* - s
* - person_id
* - hide_form (hide the form to filter the tasks)
* - status: date state, amongst SingleTaskRepository::DATE_STATUSES, or 'closed'.
*
* @Route(
* "/{_locale}/task/single-task/list",
* name="chill_task_singletask_list"
* )
*/
public function listAction(
Request $request
) {
$this->denyAccessUnlessGranted(TaskVoter::SHOW, null);
$filterOrder = $this->buildFilterOrder();
$flags = array_merge(
$filterOrder->getCheckboxData('status'),
array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states'))
);
$nb = $this->singleTaskAclAwareRepository->countByAllViewable(
$filterOrder->getQueryString(),
$flags
);
$paginator = $this->paginatorFactory->create($nb);
if (0 < $nb) {
$tasks = $this->singleTaskAclAwareRepository->findByAllViewable(
$filterOrder->getQueryString(),
$flags,
$paginator->getCurrentPageFirstItemNumber(),
$paginator->getItemsPerPage(),
[
'startDate' => 'DESC',
'endDate' => 'DESC',
]
);
} else {
$tasks = [];
}
return $this->render('@ChillTask/SingleTask/List/index.html.twig', [
'tasks' => $tasks,
'paginator' => $paginator,
'filter_order' => $filterOrder,
]);
}
/**
* @Route(
* "/{_locale}/task/single-task/by-course/{id}",
* name="chill_task_singletask_by-course_list")
*/
public function listCourseTasks(
AccompanyingPeriod $course,
FormFactoryInterface $formFactory,
Request $request
): Response {
$this->denyAccessUnlessGranted(TaskVoter::SHOW, $course);
$filterOrder = $this->buildFilterOrder();
$flags = array_merge(
$filterOrder->getCheckboxData('status'),
array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states'))
);
$nb = $this->singleTaskAclAwareRepository->countByCourse(
$course,
$filterOrder->getQueryString(),
$flags
);
$paginator = $this->paginatorFactory->create($nb);
if (0 < $nb) {
$tasks = $this->singleTaskAclAwareRepository->findByCourse(
$course,
$filterOrder->getQueryString(),
$flags,
$paginator->getCurrentPageFirstItemNumber(),
$paginator->getItemsPerPage(),
[
'startDate' => 'DESC',
'endDate' => 'DESC',
]
);
} else {
$tasks = [];
}
return $this->render(
'@ChillTask/SingleTask/AccompanyingCourse/list.html.twig',
[
'tasks' => $tasks,
'accompanyingCourse' => $course,
'paginator' => $paginator,
'filter_order' => $filterOrder,
]
);
}
/**
* @Route(
* "/{_locale}/task/single-task/by-person/{id}",
* name="chill_task_singletask_by-person_list")
*/
public function listPersonTasks(
Person $person
): Response {
$this->denyAccessUnlessGranted(TaskVoter::SHOW, $person);
$filterOrder = $this->buildFilterOrder();
$flags = array_merge(
$filterOrder->getCheckboxData('status'),
array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states'))
);
$nb = $this->singleTaskAclAwareRepository->countByPerson(
$person,
$filterOrder->getQueryString(),
$flags
);
$paginator = $this->paginatorFactory->create($nb);
if (0 < $nb) {
$tasks = $this->singleTaskAclAwareRepository->findByPerson(
$person,
$filterOrder->getQueryString(),
$flags,
$paginator->getCurrentPageFirstItemNumber(),
$paginator->getItemsPerPage(),
[
'startDate' => 'DESC',
'endDate' => 'DESC',
]
);
} else {
$tasks = [];
}
return $this->render(
'@ChillTask/SingleTask/Person/list.html.twig',
[
'tasks' => $tasks,
'person' => $person,
'paginator' => $paginator,
'filter_order' => $filterOrder,
]
);
}
/**
* @return Response
* @Route(
* "/{_locale}/task/single-task/list/my",
* name="chill_task_singletask_my_tasks",
* defaults={"_format": "html"}
* )
* @Route(
* "/api/1.0/task/single-task/list/my",
* defaults={"_format": "json"}
* )
*/
public function myTasksAction(string $_format, Request $request)
{
$this->denyAccessUnlessGranted('ROLE_USER');
$filterOrder = $this->buildFilterOrder();
$flags = array_merge(
$filterOrder->getCheckboxData('status'),
array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states'))
);
$nb = $this->singleTaskAclAwareRepository->countByCurrentUsersTasks(
$filterOrder->getQueryString(),
$flags
);
if ('json' === $_format && $request->query->getBoolean('countOnly')) {
return $this->json(
new Counter($nb),
);
}
$paginator = $this->paginatorFactory->create($nb);
$tasks = $this->singleTaskAclAwareRepository->findByCurrentUsersTasks(
$filterOrder->getQueryString(),
$flags,
$paginator->getCurrentPageFirstItemNumber(),
$paginator->getItemsPerPage(),
[
'startDate' => 'DESC',
'endDate' => 'DESC',
]
);
switch ($_format) {
case 'html':
return $this->render('@ChillTask/SingleTask/List/index_my_tasks.html.twig', [
'tasks' => $tasks,
'paginator' => $paginator,
'filter_order' => $filterOrder,
]);
case 'json':
$collection = new Collection($tasks, $paginator);
return $this->json(
$collection,
JsonResponse::HTTP_OK,
[],
['groups' => ['read']]
);
default:
throw new BadRequestHttpException("format not supported: {$_format}");
}
}
/**
* @Route(
* "/{_locale}/task/single-task/new",
* name="chill_task_single_task_new"
* )
*/
public function newAction(Request $request)
{
$task = (new SingleTask())
->setAssignee($this->getUser())
->setType('task_default');
$entityType = $this->getEntityContext($request);
if (null === $entityType) {
throw new BadRequestHttpException('You must provide a entity_type');
}
$entityId = $request->query->getInt("{$entityType}_id", 0);
if (null === $entityId) {
return new BadRequestHttpException("You must provide a {$entityType}_id");
}
switch ($entityType) {
case 'person':
$person = $this->getDoctrine()->getManager()
->getRepository(Person::class)
->find($entityId);
if (null === $person) {
$this->createNotFoundException('Invalid person id');
}
$task->setPerson($person);
$role = TaskVoter::CREATE_PERSON;
break;
case 'course':
$course = $this->getDoctrine()->getManager()
->getRepository(AccompanyingPeriod::class)
->find($entityId);
if (null === $course) {
$this->createNotFoundException('Invalid accompanying course id');
}
$task->setCourse($course);
$role = TaskVoter::CREATE_COURSE;
break;
default:
return new BadRequestHttpException("context with {$entityType} is not supported");
}
$this->denyAccessUnlessGranted($role, $task, 'You are not '
. 'allowed to create this task');
$form = $this->setCreateForm($task, $role);
$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', $this->translator->trans('The task is created'));
if ($request->query->has('returnPath')) {
return $this->redirect($request->query->get('returnPath'));
}
if ('person' === $entityType) {
return $this->redirectToRoute('chill_task_singletask_by-person_list', [
'id' => $task->getPerson()->getId(),
]);
}
if ('course' === $entityType) {
return $this->redirectToRoute('chill_task_singletask_by-course_list', [
'id' => $task->getCourse()->getId(),
]);
}
} else {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
}
switch ($entityType) {
case 'person':
return $this->render('@ChillTask/SingleTask/Person/new.html.twig', [
'form' => $form->createView(),
'task' => $task,
'person' => $task->getPerson(),
]);
case 'course':
return $this->render('@ChillTask/SingleTask/AccompanyingCourse/new.html.twig', [
'form' => $form->createView(),
'task' => $task,
'accompanyingCourse' => $task->getCourse(),
]);
default:
throw new LogicException('entity context not supported');
}
}
/**
* @Route(
* "/{_locale}/task/single-task/{id}/show",
* name="chill_task_single_task_show"
* )
*/
public function showAction(SingleTask $task, Request $request)
{
$this->denyAccessUnlessGranted(TaskVoter::SHOW, $task);
if ($task->getContext() instanceof Person) {
$event = new PrivacyEvent($task->getContext(), [
'element_class' => SingleTask::class,
'element_id' => $task->getId(),
'action' => 'show',
]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
}
$timeline = $this->timelineBuilder
->getTimelineHTML('task', ['task' => $task]);
if ($task->getContext() instanceof Person) {
return $this->render('@ChillTask/SingleTask/Person/show.html.twig', [
'task' => $task,
'timeline' => $timeline,
]);
}
return $this->render('@ChillTask/SingleTask/AccompanyingCourse/show.html.twig', [
'task' => $task,
'timeline' => $timeline,
]);
}
/**
* @return \Symfony\Component\Form\FormInterface
*/
protected function setCreateForm(SingleTask $task, string $role)
{
$form = $this->createForm(SingleTaskType::class, $task, [
'role' => $role,
]);
$form->add('submit', SubmitType::class);
return $form;
}
private function buildFilterOrder(): FilterOrderHelper
{
$statuses = ['no-alert', 'warning', 'alert'];
$statusTrans = [
'Tasks without alert',
'Tasks near deadline',
'Tasks over deadline',
];
$states = [
// todo: get a list of possible states dynamically
'new', 'in_progress', 'closed', 'canceled',
];
return $this->filterOrderHelperFactory
->create(self::class)
->addSearchBox()
->addCheckbox('status', $statuses, $statuses, $statusTrans)
->addCheckbox('states', $states, ['new', 'in_progress'])
->addUserPickers('userPicker', 'userPicker', ['multiple' => True])
->build();
}
/**
* Creates a form to delete a Task entity by id.
* @param mixed $id
*/
private function createDeleteForm($id): FormInterface
{
return $this->createFormBuilder()
->setAction($this->generateUrl(
'chill_task_single_task_delete',
['id' => $id]
))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
}
private function getEntityContext(Request $request)
{
if ($request->query->has('person_id')) {
return 'person';
}
if ($request->query->has('course_id')) {
return 'course';
}
return null;
}
}