Merge remote-tracking branch 'origin/master' into 232_resources_comment

This commit is contained in:
Julien Fastré 2022-01-10 21:56:36 +01:00
commit 8e012982f5
10 changed files with 328 additions and 136 deletions

View File

@ -6,14 +6,20 @@
<p class="message-confirm">{{ confirm_question }}</p>
{% endif %}
{% if display_content is not empty %}
{{ display_content|raw }}
{% endif %}
{{ form_start(form) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path(cancel_route, cancel_parameters|default( { } ) ) }}" class="btn btn-cancel">
{{ 'Cancel'|trans }}
</a>
</li>
{% if cancel_route is defined %}
<li class="cancel">
<a href="{{ chill_path_forward_return_path(cancel_route, cancel_parameters|default( { } ) ) }}" class="btn btn-cancel">
{{ 'Cancel'|trans }}
</a>
</li>
{% endif %}
<li>
{{ form_widget(form.submit, { 'attr' : { 'class' : "btn btn-delete" } } ) }}
</li>

View File

@ -13,91 +13,181 @@ namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Form\AccompanyingCourseCommentType;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodCommentVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\ORM\EntityManagerInterface;
use LogicException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use function array_key_exists;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingCourseCommentController extends AbstractController
{
private EntityManagerInterface $entityManager;
private FormFactoryInterface $formFactory;
private TranslatorInterface $translator;
public function __construct(
EntityManagerInterface $entityManager,
FormFactoryInterface $formFactory,
TranslatorInterface $translator
) {
$this->entityManager = $entityManager;
$this->formFactory = $formFactory;
$this->translator = $translator;
}
/**
* Comments page of Accompanying Course section.
* Page of comments in Accompanying Course section.
*
* @Route("/{_locale}/parcours/{accompanying_period_id}/comments", name="chill_person_accompanying_period_comment_list")
* @ParamConverter("accompanyingCourse", options={"id": "accompanying_period_id"})
*/
public function commentAction(AccompanyingPeriod $accompanyingCourse, Request $request): Response
{
$newComment = new AccompanyingPeriod\Comment();
$newComment->setAccompanyingPeriod($accompanyingCourse);
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE_DETAILS, $accompanyingCourse);
$form = $this->createCommentForm($newComment, 'new');
$editForm = null;
$em = $this->getDoctrine()->getManager();
$afterSuccessfulRedirectResponse = $this->redirectToRoute('chill_person_accompanying_period_comment_list', [
'accompanying_period_id' => $accompanyingCourse->getId(),
]);
if ($request->query->has('edit')) {
if ($request->query->has('edit') && $this->isGranted(AccompanyingPeriodVoter::EDIT, $accompanyingCourse)) {
foreach ($accompanyingCourse->getComments() as $comment) {
if ($comment->getId() === $request->query->getInt('edit')) {
$editForm = $this->createCommentForm($comment, 'edit');
$commentEditId = $comment->getId();
$commentEdited = $comment;
}
}
$pinnedComment = $accompanyingCourse->getPinnedComment();
if ($pinnedComment->getId() === $request->query->getInt('edit')) {
if (null !== $pinnedComment && $pinnedComment->getId() === $request->query->getInt('edit')) {
$editForm = $this->createCommentForm($pinnedComment, 'edit');
$commentEditId = $pinnedComment->getId();
$commentEdited = $pinnedComment;
}
}
if (null === $editForm) {
throw new NotFoundHttpException('Unable to find an edit form.');
}
if (!isset($editForm)) {
throw new NotFoundHttpException('comment with id ' . $request->query->getInt('edit') . ' is not found');
}
if ($request->getMethod() === Request::METHOD_POST) {
$currentForm = $editForm->handleRequest($request);
if (array_key_exists('edit', $request->request->all()[$editForm->getName()])) {
$isEditingNew = false;
if (isset($commentEdited)) {
$this->denyAccessUnlessGranted(AccompanyingPeriodCommentVoter::EDIT, $commentEdited);
} else {
$isEditingNew = true;
throw new LogicException('at this step, commentEdited should be set');
}
if ($currentForm->isSubmitted() && $currentForm->isValid()) {
$em = $this->getDoctrine()->getManager();
$editForm->handleRequest($request);
if ($isEditingNew) {
$em->persist($newComment);
}
if ($editForm->isSubmitted() && $editForm->isValid()) {
$em->flush();
return $afterSuccessfulRedirectResponse;
}
return $this->redirectToRoute('chill_person_accompanying_period_comment_list', [
'accompanying_period_id' => $accompanyingCourse->getId(),
]);
if ($editForm->isSubmitted() && !$editForm->isValid()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
}
return $this->render('@ChillPerson/AccompanyingCourse/comment_list.html.twig', [
if ($this->isGranted(AccompanyingPeriodVoter::EDIT, $accompanyingCourse)) {
$newComment = new AccompanyingPeriod\Comment();
$newComment->setAccompanyingPeriod($accompanyingCourse);
$newForm = $this->createCommentForm($newComment, 'new');
$newForm->handleRequest($request);
if ($newForm->isSubmitted() && $newForm->isValid()) {
$em->persist($newComment);
$em->flush();
return $afterSuccessfulRedirectResponse;
}
}
return $this->render('@ChillPerson/AccompanyingCourse/Comment/index.html.twig', [
'accompanyingCourse' => $accompanyingCourse,
'form' => $form->createView(),
'edit_form' => null !== $editForm ? $editForm->createView() : null,
'form' => isset($newForm) ? $newForm->createView() : null,
'edit_form' => isset($editForm) ? $editForm->createView() : null,
'commentEditId' => $commentEditId ?? null,
]);
}
/**
* Delete an existing comment.
*
* @Route(
* "/{_locale}/parcours/comment/{id}/delete",
* name="chill_person_accompanying_period_comment_delete"
* )
*/
public function deleteAction(AccompanyingPeriod\Comment $comment, Request $request): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodCommentVoter::DELETE, $comment);
$form = $this->createForm(FormType::class, []);
$form->add('submit', SubmitType::class, ['label' => 'Confirm']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->remove($comment);
if ($comment->getAccompanyingPeriod()->getPinnedComment() === $comment) {
$comment->getAccompanyingPeriod()->setPinnedComment(null);
}
$this->entityManager->flush();
$this->addFlash('success', $this->translator->trans('accompanying_course_comment.Comment removed'));
return $this->redirect(
$request->query->has('returnPath') ? $request->query->get('returnPath') :
$this->generateUrl('chill_person_accompanying_period_comment_list', [
'accompanying_period_id' => $comment->getAccompanyingPeriod()->getId(),
])
);
}
return $this->render('@ChillPerson/AccompanyingCourse/Comment/confirm_delete.html.twig', [
'comment' => $comment,
'delete_form' => $form->createView(),
'accompanyingCourse' => $comment->getAccompanyingPeriod(),
]);
}
/**
* @Route("/{_locale}/parcours/comment/{id}/pin", name="chill_person_accompanying_period_comment_pin")
*/
public function pinComment(AccompanyingPeriod\Comment $comment): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::EDIT, $comment->getAccompanyingPeriod());
$comment->getAccompanyingPeriod()->setPinnedComment($comment);
$this->getDoctrine()->getManager()->flush();
$this->addFlash('success', $this->translator->trans('accompanying_course.comment is pinned'));
return $this->redirectToRoute('chill_person_accompanying_period_comment_list', [
'accompanying_period_id' => $comment->getAccompanyingPeriod()->getId(),
]);
}
private function createCommentForm(AccompanyingPeriod\Comment $comment, string $step): FormInterface
{
$form = $this->createForm(AccompanyingCourseCommentType::class, $comment);
if ('edit' === $step) {
$form->add('edit', HiddenType::class, ['mapped' => false]);
}
return $form;
return $this->formFactory->createNamed($step, AccompanyingCourseCommentType::class, $comment);
}
}

View File

@ -24,7 +24,9 @@ class AccompanyingCourseCommentType extends AbstractType
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('content', ChillTextareaType::class, []);
$builder->add('content', ChillTextareaType::class, [
'required' => false,
]);
}
/**

View File

@ -0,0 +1,19 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% block title 'accompanying_course_comment.Remove comment'|trans %}
{% block display_content %}
<blockquote class="chill-user-quote col">{{ comment.content|chill_markdown_to_html }}</blockquote>
{% endblock %}
{% block content %}
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'accompanying_course_comment.Remove comment'|trans,
'confirm_question' : 'accompanying_course_comment.Are you sure you want to remove comment ?'|trans,
'cancel_route' : 'chill_person_accompanying_period_comment_list',
'cancel_parameters' : { 'accompanying_period_id': comment.accompanyingPeriod.id },
'display_content' : block('display_content'),
'form' : delete_form
} ) }}
{% endblock %}

View File

@ -0,0 +1,91 @@
{% extends '@ChillPerson/AccompanyingCourse/layout.html.twig' %}
{% block title %}
{{ 'Accompanying Course Comment list'|trans }}
{% endblock %}
{% import '@ChillPerson/AccompanyingCourse/Comment/macro_showItem.html.twig' as m %}
{% macro recordAction(comment, isPinned) %}
{% if isPinned is defined and isPinned == 'true' %}
{% else %}
<li>
<form method="post" action="{{ chill_path_forward_return_path('chill_person_accompanying_period_comment_pin', {'id': comment.id}) }}">
<button class="btn btn-sm btn-misc" type="submit">
<i class="fa fa-flag fa-fw"></i>{{ 'Pin comment'|trans }}
</button>
</form>
</li>
{% endif %}
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_COMMENT_EDIT', comment) %}
<li>
<a class="btn btn-sm btn-edit" title="{{ 'Edit'|trans }}" href="{{ path('chill_person_accompanying_period_comment_list', {
'_fragment': 'comment-' ~ comment.id,
'edit': comment.id,
'accompanying_period_id': comment.accompanyingPeriod.id
}) }}"></a>
</li>
{% endif %}
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_COMMENT_DELETE', comment) %}
<li>
<a class="btn btn-sm btn-delete" title="{{ 'Delete'|trans }}" href="{{ path('chill_person_accompanying_period_comment_delete', {'id': comment.id}) }}"></a>
</li>
{% endif %}
{% endmacro %}
{% macro form_comment(type, form) %}
{% if type == 'edit' %}
<div class="item-bloc">
<div class="item-row row">
{% endif %}
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_widget(form.content) }}
<ul class="record_actions">
<li>
{% if type == 'new' %}
<button class="btn btn-create" type="submit">{{ 'Post a new comment'|trans }}</button>
{% elseif type == 'edit' %}
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
{% endif %}
</li>
</ul>
{{ form_end(form) }}
{% if type == 'edit' %}
</div>
</div>
{% endif %}
{% endmacro %}
{% block content %}
<div class="accompanyingcourse-comment-list">
<h1>{{ block('title') }}</h1>
<div class="flex-table">
{% if accompanyingCourse.pinnedComment %}
{% if commentEditId == accompanyingCourse.pinnedComment.id %}
{{ _self.form_comment('edit', edit_form) }}
{% else %}
{{ m.show_comment(accompanyingCourse.pinnedComment, {
'pinned': 'true',
'recordAction': _self.recordAction(accompanyingCourse.pinnedComment, 'true')
}) }}
{% endif %}
{% endif %}
{% for comment in accompanyingCourse.comments %}
{% if commentEditId == comment.id %}
{{ _self.form_comment('edit', edit_form) }}
{% else %}
{{ m.show_comment(comment, {
'recordAction': _self.recordAction(comment)
}) }}
{% endif %}
{% endfor %}
</div>
{% if form is not null %}
<div class="new-comment my-5">
<h2 class="chill-blue">{{ 'Write a new comment'|trans }}</h2>
{{ _self.form_comment('new', form) }}
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% macro show_comment(comment, options) %}
<div class="item-bloc">
<div class="item-row">
<div>
{% if options.pinned is defined %}
<i class="fa fa-flag fa-fw fa-lg" title="{{ 'pinned'|trans }}"></i>
{% endif %}
<a id="comment-{{ comment.id }}" href="{{ '#comment-' ~ comment.id }}" class="fa fa-pencil-square-o fa-fw"></a>
{% set creator = comment.creator is defined ? comment.creator : comment.createdBy %}
{{ 'by'|trans }}<b>{{ creator }}</b>
{{ ', ' ~ 'on'|trans ~ ' ' ~ comment.createdAt|format_date('long') }}<br>
<i>{{ 'Last updated on'|trans ~ ' ' ~ comment.updatedAt|format_datetime('long', 'short') }}</i>
</div>
<ul class="record_actions">
{{ options.recordAction }}
</ul>
</div>
<div class="item-row separator">
<blockquote class="chill-user-quote col">{{ comment.content|chill_markdown_to_html }}</blockquote>
</div>
</div>
{% endmacro %}

View File

@ -1,92 +0,0 @@
{% extends '@ChillPerson/AccompanyingCourse/layout.html.twig' %}
{% block title %}
{{ 'Accompanying Course Comment list'|trans }}
{% endblock %}
{% macro show_comment(comment, options) %}
<div class="item-bloc">
<div class="item-row">
<div>
{% if options.pinned is defined %}
<i class="fa fa-flag fa-fw fa-lg" title="{{ 'pinned'|trans }}"></i>
{% endif %}
<a id="comment{{ comment.id }}" href="{{ '#comment' ~ comment.id }}" class="fa fa-pencil-square-o fa-fw"></a>
{{ 'by'|trans }}<b>{{ comment.creator }}</b>{{ ', ' ~ 'on'|trans ~ ' ' ~ comment.createdAt|format_date('long') }}<br>
<i>{{ 'Last updated on'|trans ~ ' ' ~ comment.updatedAt|format_datetime('long', 'short') }}</i>
</div>
<ul class="record_actions">
{% if options.pinned is not defined %}
<li>
<button class="btn btn-sm btn-misc" type="button">
<i class="fa fa-flag fa-fw"></i>
Épingler
</button>
</li>
{% endif %}
<li>
<a class="btn btn-sm btn-edit" title="{{ 'Edit'|trans }}" href="{{ path('chill_person_accompanying_period_comment_list', {
'accompanying_period_id': comment.accompanyingPeriod.id,
'edit': comment.id
}) ~ '#comment' ~ comment.id }}"></a>
</li>
<li>
<a class="btn btn-sm btn-delete" title="{{ 'Delete'|trans }}" href=""></a>
</li>
</ul>
</div>
<div class="item-row separator">
<blockquote class="chill-user-quote col">{{ comment.content|chill_markdown_to_html }}</blockquote>
</div>
</div>
{% endmacro %}
{% macro form_comment(type, form) %}
{% if type == 'edit' %}
<div class="item-bloc">
<div class="item-row row">
{% endif %}
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_widget(form.content) }}
<ul class="record_actions">
<li>
{% if type == 'new' %}
<button class="btn btn-create" type="submit">{{ 'Post a new comment'|trans }}</button>
{% elseif type == 'edit' %}
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
{% endif %}
</li>
</ul>
{{ form_end(form) }}
{% if type == 'edit' %}
</div>
</div>
{% endif %}
{% endmacro %}
{% block content %}
<div class="accompanyingcourse-comment-list">
<h1>{{ block('title') }}</h1>
<div class="flex-table">
{% if accompanyingCourse.pinnedComment %}
{% if commentEditId == accompanyingCourse.pinnedComment.id %}
{{ _self.form_comment('edit', edit_form) }}
{% else %}
{{ _self.show_comment(accompanyingCourse.pinnedComment, {'pinned': 'true'}) }}
{% endif %}
{% endif %}
{% for c in accompanyingCourse.comments %}
{% if commentEditId == c.id %}
{{ _self.form_comment('edit', edit_form) }}
{% else %}
{{ _self.show_comment(c) }}
{% endif %}
{% endfor %}
</div>
<div class="new-comment my-5">
<h2 class="chill-blue">{{ 'Write a new comment'|trans }}</h2>
{{ _self.form_comment('new', form) }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,42 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Security\Authorization;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use UnexpectedValueException;
class AccompanyingPeriodCommentVoter extends Voter
{
public const DELETE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_COMMENT_DELETE';
public const EDIT = 'CHILL_PERSON_ACCOMPANYING_PERIOD_COMMENT_EDIT';
protected function supports($attribute, $subject)
{
return $subject instanceof Comment;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** @var Comment $subject */
switch ($attribute) {
case self::EDIT:
case self::DELETE:
return $subject->getCreator() === $token->getUser();
default:
throw new UnexpectedValueException("This attribute {$attribute} is not supported");
}
}
}

View File

@ -12,3 +12,7 @@ services:
- { name: security.voter }
- { name: chill.role }
Chill\PersonBundle\Security\Authorization\AccompanyingPeriodCommentVoter:
autowire: true
tags:
- { name: security.voter }

View File

@ -413,14 +413,20 @@ Locate by: Localiser auprès de
fix it: Compléter
accompanying_course:
administrative_location: Localisation administrative
comment is pinned: Le commentaire est épinglé
# Accompanying Course comments
Accompanying Course Comment: Commentaire
Accompanying Course Comment list: Commentaires du parcours
pinned: épinglé
Pin comment: Épingler
Post a new comment: Poster un nouveau commentaire
Write a new comment: Écrire un nouveau commentaire
Edit a comment: Modifier le commentaire
accompanying_course_comment:
Comment removed: Commentaire supprimé
Remove comment: Supprimer le commentaire
Are you sure you want to remove comment ?: Étes-vous sûr de vouloir supprimer ce commentaire ?
# Household
Household: Ménage