Commit cd24aaf0 authored by Emma's avatar Emma 🏳🌈

improve comment-related things (aesthetics/code)

parent 9acc6fd8
Pipeline #60740078 passed with stages
in 15 minutes and 36 seconds
......@@ -43,6 +43,10 @@
padding-top: 0.5rem;
}
&__vote {
padding-bottom: 0.5rem;
}
&__main {
flex-grow: 1;
min-width: 0;
......@@ -65,6 +69,7 @@
}
&__body {
min-height: 1rem;
padding-right: 0.5rem;
}
......
......@@ -26,7 +26,7 @@ comment_form:
path: /comment_form/{forumName}/{submissionId}/{commentId}
requirements: { submissionId: \d+, commentId: \d+ }
delete_comment:
delete_comment_thread:
controller: App\Controller\CommentController::deleteComment
defaults: { slug: '-' }
path: /f/{forum_name}/{submission_id}/{slug}/comment/{comment_id}/delete
......
......@@ -14,7 +14,7 @@ use App\Form\Model\CommentData;
use App\Repository\CommentRepository;
use App\Repository\ForumRepository;
use App\Utils\Slugger;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
......@@ -29,28 +29,48 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
* @Entity("comment", expr="repository.findOneBySubmissionAndIdOr404(submission, comment_id)")
*/
final class CommentController extends AbstractController {
public function list(CommentRepository $repository, int $page) {
/**
* @var CommentRepository
*/
private $comments;
/**
* @var EntityManagerInterface
*/
private $entityManager;
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var ForumRepository
*/
private $forums;
public function __construct(
CommentRepository $comments,
EntityManagerInterface $entityManager,
EventDispatcherInterface $eventDispatcher,
ForumRepository $forums
) {
$this->comments = $comments;
$this->entityManager = $entityManager;
$this->eventDispatcher = $eventDispatcher;
$this->forums = $forums;
}
public function list(int $page) {
return $this->render('comment/list.html.twig', [
'comments' => $repository->findRecentPaginated($page),
'comments' => $this->comments->findRecentPaginated($page),
]);
}
/**
* Render the comment form only (no layout).
*
* @param ForumRepository $forumRepository
* @param string $forumName
* @param int $submissionId
* @param int|null $commentId
*
* @return Response
*/
public function commentForm(
ForumRepository $forumRepository,
$forumName,
$submissionId,
$commentId = null
) {
public function commentForm($forumName, $submissionId, $commentId = null): Response {
$routeParams = [
'forum_name' => $forumName,
'submission_id' => $submissionId,
......@@ -64,7 +84,7 @@ final class CommentController extends AbstractController {
$form = $this->createNamedForm($name, CommentType::class, null, [
'action' => $this->generateUrl('comment_post', $routeParams),
'forum' => $forumRepository->findOneByCaseInsensitiveName($forumName),
'forum' => $this->forums->findOneByCaseInsensitiveName($forumName),
]);
return $this->render('comment/form_fragment.html.twig', [
......@@ -76,24 +96,8 @@ final class CommentController extends AbstractController {
* Submit a comment. This is intended for users without JS enabled.
*
* @IsGranted("ROLE_USER")
*
* @param EntityManager $em
* @param Forum $forum
* @param Submission $submission
* @param Comment|null $comment
* @param Request $request
* @param EventDispatcherInterface $dispatcher
*
* @return Response
*/
public function comment(
EntityManager $em,
Forum $forum,
Submission $submission,
?Comment $comment,
Request $request,
EventDispatcherInterface $dispatcher
) {
public function comment(Forum $forum, Submission $submission, ?Comment $comment, Request $request) {
$name = $this->getFormName($submission, $comment);
$data = new CommentData($submission);
......@@ -105,10 +109,10 @@ final class CommentController extends AbstractController {
if ($form->isSubmitted() && $form->isValid()) {
$reply = $data->toComment($this->getUser(), $comment, $request->getClientIp());
$em->persist($reply);
$em->flush();
$this->entityManager->persist($reply);
$this->entityManager->flush();
$dispatcher->dispatch(Events::NEW_COMMENT, new GenericEvent($reply));
$this->eventDispatcher->dispatch(Events::NEW_COMMENT, new GenericEvent($reply));
return $this->redirectToRoute('comment', [
'forum_name' => $forum->getName(),
......@@ -138,24 +142,8 @@ final class CommentController extends AbstractController {
*
* @IsGranted("ROLE_USER")
* @IsGranted("edit", subject="comment", statusCode=403)
*
* @param EntityManager $em
* @param Forum $forum
* @param Submission $submission
* @param Comment $comment
* @param Request $request
* @param EventDispatcherInterface $dispatcher
*
* @return Response
*/
public function editComment(
EntityManager $em,
Forum $forum,
Submission $submission,
Comment $comment,
Request $request,
EventDispatcherInterface $dispatcher
) {
public function editComment(Forum $forum, Submission $submission, Comment $comment, Request $request) {
$data = CommentData::createFromComment($comment);
$form = $this->createForm(CommentType::class, $data, ['forum' => $forum]);
......@@ -165,10 +153,10 @@ final class CommentController extends AbstractController {
$before = clone $comment;
$data->updateComment($comment, $this->getUser());
$em->flush();
$this->entityManager->flush();
$event = new EntityModifiedEvent($before, $comment);
$dispatcher->dispatch(Events::EDIT_COMMENT, $event);
$this->eventDispatcher->dispatch(Events::EDIT_COMMENT, $event);
return $this->redirectToRoute('comment', [
'forum_name' => $forum->getName(),
......@@ -187,42 +175,22 @@ final class CommentController extends AbstractController {
}
/**
* Delete a comment.
* Delete a comment thread.
*
* @IsGranted("ROLE_USER")
* @IsGranted("delete", subject="comment", statusCode=403)
*
* @param EntityManager $em
* @param Submission $submission
* @param Forum $forum
* @param Comment $comment
* @param Request $request
*
* @return Response
* @IsGranted("delete_thread", subject="comment", statusCode=403)
*/
public function deleteComment(
EntityManager $em,
Submission $submission,
Forum $forum,
Comment $comment,
Request $request
) {
public function deleteComment(Submission $submission, Forum $forum, Comment $comment, Request $request): Response {
$this->validateCsrf('delete_comment', $request->request->get('token'));
if ($this->isGranted('delete_thread', $comment)) {
$submission->removeComment($comment);
$em->remove($comment);
} elseif ($this->isGranted('softdelete', $comment)) {
$comment->softDelete();
} else {
throw new \RuntimeException("This shouldn't happen");
}
$submission->removeComment($comment);
$this->entityManager->remove($comment);
$this->logDeletion($forum, $comment);
$commentId = $comment->getId(); // not available on entity after flush()
$em->flush();
$this->entityManager->flush();
if ($request->headers->has('Referer')) {
$commentUrl = $this->generateUrl('comment', [
......@@ -247,29 +215,15 @@ final class CommentController extends AbstractController {
*
* @IsGranted("ROLE_USER")
* @IsGranted("softdelete", subject="comment", statusCode=403)
*
* @param EntityManager $em
* @param Forum $forum
* @param Submission $submission
* @param Comment $comment
* @param Request $request
*
* @return Response
*/
public function softDeleteComment(
EntityManager $em,
Forum $forum,
/* @noinspection PhpUnusedParameterInspection */ Submission $submission,
Comment $comment,
Request $request
) {
public function softDeleteComment(Forum $forum, Submission $submission, Comment $comment, Request $request): Response {
$this->validateCsrf('softdelete_comment', $request->request->get('token'));
$comment->softDelete();
$this->logDeletion($forum, $comment);
$em->flush();
$this->entityManager->flush();
return $this->redirectAfterAction($comment, $request);
}
......
......@@ -321,15 +321,16 @@ class Comment extends Votable {
}
public function isSoftDeleted(): bool {
return $this->softDeleted;
return $this->softDeleted && $this->body === '';
}
/**
* Delete a comment without deleting its replies.
*/
public function softDelete() {
public function softDelete(): void {
$this->softDeleted = true;
$this->body = '';
$this->userFlag = 0;
$this->submission->updateCommentCount();
$this->submission->updateRanking();
$this->submission->updateLastActive();
......
......@@ -10,57 +10,28 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class CommentVoter extends Voter {
/**
* - delete - Allowed to delete a thread *or* to soft-delete a comment.
* - delete_thread - Ability to delete a comment with its replies.
* - softdelete - Ability to soft delete a comment.
* - edit - Ability to edit a comment.
*/
const ATTRIBUTES = ['delete', 'delete_thread', 'softdelete', 'edit'];
const ATTRIBUTES = ['delete_thread', 'softdelete', 'edit'];
/**
* @var AccessDecisionManagerInterface
*/
private $decisionManager;
/**
* @param AccessDecisionManagerInterface $decisionManager
*/
public function __construct(AccessDecisionManagerInterface $decisionManager) {
$this->decisionManager = $decisionManager;
}
/**
* {@inheritdoc}
*/
protected function supports($attribute, $subject) {
if (!in_array($attribute, self::ATTRIBUTES)) {
return false;
}
if (!$subject instanceof Comment) {
return false;
}
return true;
return \in_array($attribute, self::ATTRIBUTES, true) && $subject instanceof Comment;
}
/**
* {@inheritdoc}
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
if (!$token->getUser() instanceof User) {
return false;
}
if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) {
return true;
}
switch ($attribute) {
case 'delete':
return
$this->canDeleteThread($subject, $token) ||
$this->canSoftDelete($subject, $token);
case 'delete_thread':
return $this->canDeleteThread($subject, $token);
case 'softdelete':
......@@ -72,69 +43,63 @@ final class CommentVoter extends Voter {
}
}
/**
* @param Comment $comment
* @param TokenInterface $token
*
* @return bool
*/
private function canDeleteThread(Comment $comment, TokenInterface $token) {
private function canDeleteThread(Comment $comment, TokenInterface $token): bool {
$forum = $comment->getSubmission()->getForum();
// moderators can delete threads with or without replies
if ($forum->userIsModerator($token->getUser())) {
return true;
}
// non-forum mods and non-admins cannot delete threads with replies
if (count($comment->getChildren()) > 0) {
if ($token->getUser() !== $comment->getUser()) {
return false;
}
if (\count($comment->getChildren()) > 0) {
return false;
}
// users can delete their own comments
return $token->getUser() === $comment->getUser();
return true;
}
/**
* @param Comment $comment
* @param TokenInterface $token
*
* @return bool
*/
private function canSoftDelete(Comment $comment, TokenInterface $token) {
// users can delete their own comments
private function canSoftDelete(Comment $comment, TokenInterface $token): bool {
if ($comment->isSoftDeleted()) {
return false;
}
if (\count($comment->getChildren()) === 0) {
return false;
}
if ($token->getUser() === $comment->getUser()) {
return true;
}
$forum = $comment->getSubmission()->getForum();
// moderators can soft-delete
return $forum->userIsModerator($token->getUser());
if (!$forum->userIsModerator($token->getUser())) {
return false;
}
return true;
}
/**
* @param Comment $comment
* @param TokenInterface $token
*
* @return bool
*/
private function canEdit(Comment $comment, TokenInterface $token) {
private function canEdit(Comment $comment, TokenInterface $token): bool {
if ($comment->isSoftDeleted()) {
return false;
}
$forum = $comment->getSubmission()->getForum();
if ($forum->userIsModerator($token->getUser())) {
if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) {
return true;
}
// users can edit their own comments
if ($token->getUser() === $comment->getUser()) {
return !$comment->isModerated();
if ($token->getUser() !== $comment->getUser()) {
return false;
}
if ($comment->isModerated()) {
return false;
}
return false;
return true;
}
}
......@@ -106,7 +106,7 @@
{%- endif -%}
{% endfilter %}
{% if comment.user is same as(comment.submission.user) %}
{% if not comment.softDeleted and comment.user is same as(comment.submission.user) %}
<small class="comment__op-text text-sm user-flag"
title="{{ 'item.op'|trans }}"
aria-label="{{ 'item.op'|trans }}">
......@@ -124,10 +124,16 @@
{% if comment.user is same as(self) %}
{% set comment_nav_classes = 'fg-inherit text-sm' %}
{{- block('comment_nav_delete') -}}
{% if comment.children|length == 0 %}
{{ block('comment_nav_delete_thread') }}
{% endif %}
{{- block('comment_nav_softdelete') -}}
{{- block('comment_nav_edit') }}
{% elseif is_granted('moderator', comment.submission.forum) %}
{% set comment_nav_classes = 'menu-item no-wrap' %}
{% endif %}
{% set menu_actions = block('comment_nav_menu_actions')|trim %}
{% if menu_actions is not empty %}
<li class="dropdown">
<button class="dropdown__toggle fg-inherit no-underline text-sm unbuttonize">
<span class="no-underline__exempt">{{ 'nav.actions'|trans }}</span>
......@@ -135,18 +141,25 @@
</button>
<ul class="dropdown__menu dropdown-card unlistify no-margin">
{% if comment.user is not same as(self) %}
{{ block('comment_nav_delete') }}
{{ block('comment_nav_edit') }}
{% endif %}
{{ block('comment_nav_ban') }}
{{ block('comment_nav_ip_ban') }}
{{ menu_actions|raw }}
</ul>
</li>
{% endif %}
{% endblock comment_nav %}
{% block comment_nav_menu_actions %}
{% set comment_nav_classes = 'menu-item no-wrap' %}
{% if comment.user is not same as(self) %}
{{ block('comment_nav_softdelete') }}
{{ block('comment_nav_delete_thread') }}
{{ block('comment_nav_edit') }}
{{ block('comment_nav_ban') }}
{{ block('comment_nav_ip_ban') }}
{% elseif comment.children|length > 0 %}
{{ block('comment_nav_delete_thread') }}
{% endif %}
{% endblock %}
{%- block comment_nav_reply -%}
<li>
<a href="{{ path('comment', {
......@@ -189,53 +202,44 @@
{%- endfilter -%}
{%- endblock comment_nav_parent -%}
{# Forms are used here because we need to support JS-less browsing and because
# GET requests should never mutate the state of the application. #}
{%- block comment_nav_delete -%}
{%- if comment.children|length > 0 and is_granted('delete_thread', comment) -%}
{%- set delete_thread_label = 'comments.delete_thread' -%}
{{- block('comment_nav_delete_thread') -}}
{{- block('comment_nav_delete_softdelete') -}}
{%- elseif is_granted('delete', comment) -%}
{%- set delete_thread_label = 'comments.delete' -%}
{{- block('comment_nav_delete_thread') -}}
{%- endif -%}
{%- endblock comment_nav_delete -%}
{% block comment_nav_delete_softdelete %}
{% block comment_nav_softdelete %}
{% from '_macros/icon.html.twig' import icon %}
<li>
<form action="{{ path('softdelete_comment', {
forum_name: comment.submission.forum.name,
submission_id: comment.submission.id,
comment_id: comment.id,
}) }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token('softdelete_comment') }}">
<button type="submit"
class="unbuttonize comment__soft-delete-button {{ comment_nav_classes }} js-confirm-comment-delete">
{{- 'menu-item' in comment_nav_classes ? icon('trash') }}
{{ 'comments.delete'|trans -}}
</button>
</form>
</li>
{% endblock comment_nav_delete_softdelete %}
{%- block comment_nav_delete_thread -%}
{% from '_macros/icon.html.twig' import icon %}
<li>
<form action="{{ path('delete_comment', {
{% if is_granted('softdelete', comment) %}
<li>
<form action="{{ path('softdelete_comment', {
forum_name: comment.submission.forum.name,
submission_id: comment.submission.id,
comment_id: comment.id,
}) }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token('delete_comment') }}">
<button type="submit"
class="unbuttonize comment__thread-delete-button {{ comment_nav_classes }} js-confirm-comment-delete">
{{- 'menu-item' in comment_nav_classes ? icon('trash') }}
{{ delete_thread_label|trans -}}
</button>
</form>
</li>
}) }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token('softdelete_comment') }}">
<button type="submit"
class="unbuttonize comment__soft-delete-button {{ comment_nav_classes }} js-confirm-comment-delete">
{{- 'menu-item' in comment_nav_classes ? icon('trash') }}
{{ 'comments.delete'|trans -}}
</button>
</form>
</li>
{% endif %}
{% endblock comment_nav_softdelete %}
{%- block comment_nav_delete_thread -%}
{% from '_macros/icon.html.twig' import icon %}
{% if is_granted('delete_thread', comment) %}
<li>
<form action="{{ path('delete_comment_thread', {
forum_name: comment.submission.forum.name,
submission_id: comment.submission.id,
comment_id: comment.id,
}) }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token('delete_comment') }}">
<button type="submit"
class="unbuttonize comment__thread-delete-button {{ comment_nav_classes }} js-confirm-comment-delete">
{{- 'menu-item' in comment_nav_classes ? icon('trash') }}
{{ comment.children|length > 0 ? 'comments.delete_thread'|trans : 'comments.delete'|trans -}}
</button>
</form>
</li>
{% endif %}
{%- endblock comment_nav_delete_thread -%}
{%- block comment_nav_edit -%}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment