Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: faciliter la moderation des Post pour les utilisateurs staff #928

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lacommunaute/forum_conversation/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ def get_posts_of_a_topic_except_first_one(topic: Topic, user: User) -> QuerySet[

def can_certify_post(user):
return user.is_authenticated and user.is_staff


def can_moderate_post(user):
return user.is_authenticated and user.is_staff
33 changes: 18 additions & 15 deletions lacommunaute/forum_conversation/tests/tests_shortcuts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import pytest
from django.contrib.auth.models import AnonymousUser
from django.test import TestCase

from lacommunaute.forum.factories import ForumFactory
from lacommunaute.forum_conversation.factories import PostFactory, TopicFactory
from lacommunaute.forum_conversation.shortcuts import can_certify_post, get_posts_of_a_topic_except_first_one
from lacommunaute.forum_conversation.shortcuts import (
can_certify_post,
can_moderate_post,
get_posts_of_a_topic_except_first_one,
)
from lacommunaute.forum_upvote.factories import UpVoteFactory
from lacommunaute.users.factories import UserFactory

Expand Down Expand Up @@ -70,18 +74,17 @@ def test_topic_has_been_upvoted_by_the_user(self):
self.assertTrue(post.has_upvoted)


class CanCertifyPostShortcutTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = UserFactory.create()
cls.forum = ForumFactory.create()
@pytest.mark.parametrize(
"user,has_right",
[(lambda: AnonymousUser(), False), (lambda: UserFactory(), False), (lambda: UserFactory(is_staff=True), True)],
)
def test_can_certify_post(db, user, has_right):
assert can_certify_post(user()) == has_right

def test_user_is_not_authenticated(self):
self.assertFalse(can_certify_post(AnonymousUser()))

def test_user_is_staff(self):
self.user.is_staff = True
self.assertTrue(can_certify_post(self.user))

def test_user_is_not_staff(self):
self.assertFalse(can_certify_post(self.user))
@pytest.mark.parametrize(
"user,has_right",
[(lambda: AnonymousUser(), False), (lambda: UserFactory(), False), (lambda: UserFactory(is_staff=True), True)],
)
def test_can_moderate_post(db, user, has_right):
assert can_moderate_post(user()) == has_right
158 changes: 146 additions & 12 deletions lacommunaute/forum_conversation/tests/tests_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@
assign_perm = get_class("forum_permission.shortcuts", "assign_perm")


@pytest.fixture(name="topics_url")
def fixture_topics_url():
return reverse("forum_conversation_extension:topics")


@pytest.fixture(name="public_forum_with_topic")
def fixture_public_forum_with_topic(db):
forum = ForumFactory(with_public_perms=True)
TopicFactory(with_post=True, forum=forum, with_tags=["tag"])
return forum


def check_email_last_seen(email):
return EmailLastSeen.objects.filter(
email=email, last_seen_kind__in=[EmailLastSeenKind.POST, EmailLastSeenKind.LOGGED]
Expand Down Expand Up @@ -660,6 +672,140 @@ def test_redirection(self):
self.assertTrue(view.success_message, msgs._queued_messages[0].message)


class TestPostModerateView:
def get_post_moderate_url(self, topic):
return reverse(
"forum_conversation_extension:post_moderate",
kwargs={
"forum_slug": topic.forum.slug,
"forum_pk": topic.forum.pk,
"slug": topic.slug,
"pk": topic.pk,
},
)

@pytest.mark.parametrize(
"method,status_code", [("post", 302), ("get", 405), ("put", 405), ("delete", 405), ("patch", 405)]
)
def test_allowed_method(self, client, db, method, status_code, public_forum_with_topic):
topic = public_forum_with_topic.topics.first()
client.force_login(UserFactory(is_staff=True))
response = getattr(client, method)(self.get_post_moderate_url(topic), data={"post_pk": topic.last_post.pk})
assert response.status_code == status_code

@pytest.mark.parametrize(
"user,topic,status_code",
[
(
lambda: UserFactory(is_staff=True),
lambda: TopicFactory(with_post=True, forum=ForumFactory(with_public_perms=True)),
302,
),
(lambda: UserFactory(is_staff=True), lambda: TopicFactory(with_post=True), 403),
(
lambda: UserFactory(),
lambda: TopicFactory(with_post=True, forum=ForumFactory(with_public_perms=True)),
403,
),
(None, lambda: TopicFactory(with_post=True, forum=ForumFactory(with_public_perms=True)), 403),
],
)
def test_user_permission(self, client, db, user, topic, status_code):
topic = topic()
if user:
client.force_login(user())
response = client.post(self.get_post_moderate_url(topic), data={"post_pk": topic.last_post.pk})
assert response.status_code == status_code

@pytest.mark.parametrize(
"topic,expected_url",
[
(
lambda: TopicFactory(with_post=True, forum=ForumFactory(with_public_perms=True)),
lambda topic: reverse("forum_conversation_extension:topics"),
),
(
lambda: TopicFactory(
with_post=True, forum=ForumFactory(with_public_perms=True, parent=CategoryForumFactory())
),
lambda topic: reverse(
"forum_extension:forum", kwargs={"slug": topic.forum.slug, "pk": topic.forum.pk}
),
),
(
lambda: TopicFactory(with_post=True, answered=True, forum=ForumFactory(with_public_perms=True)),
lambda topic: reverse(
"forum_conversation:topic",
kwargs={
"forum_slug": topic.forum.slug,
"forum_pk": topic.forum.pk,
"slug": topic.slug,
"pk": topic.pk,
},
),
),
],
)
def test_redirection(self, client, db, topic, expected_url):
topic = topic()
expected_url = expected_url(topic=topic)
client.force_login(UserFactory(is_staff=True))
response = client.post(self.get_post_moderate_url(topic), data={"post_pk": topic.last_post.pk})
assert response.url == expected_url

@pytest.mark.parametrize(
"post_exists,kwargs,status_code",
[
(
True,
lambda topic: {
"forum_slug": topic.forum.slug,
"forum_pk": topic.forum.pk,
"slug": topic.slug,
"pk": topic.pk,
},
302,
),
(
False,
{
"forum_slug": faker.slug(),
"forum_pk": 99999,
"slug": faker.slug(),
"pk": 99999,
},
404,
),
],
)
def test_post_existence(self, client, db, post_exists, kwargs, status_code, public_forum_with_topic):
if post_exists:
topic = public_forum_with_topic.topics.first()
kwargs = kwargs(topic=topic)
last_post_pk = topic.last_post.pk
else:
kwargs = kwargs
last_post_pk = 9999
client.force_login(UserFactory(is_staff=True))
url = reverse("forum_conversation_extension:post_moderate", kwargs=kwargs)
response = client.post(url, data={"post_pk": last_post_pk})
assert response.status_code == status_code

def test_pk_is_missing_in_payload(self, client, db, public_forum_with_topic):
topic = public_forum_with_topic.topics.first()
client.force_login(UserFactory(is_staff=True))
response = client.post(self.get_post_moderate_url(topic), data={})
assert response.status_code == 404

def test_post_is_unapproved(self, client, db, public_forum_with_topic):
topic = public_forum_with_topic.topics.first()
client.force_login(UserFactory(is_staff=True))
response = client.post(self.get_post_moderate_url(topic), data={"post_pk": topic.last_post.pk})
assert response.status_code == 302
topic.last_post.refresh_from_db()
assert not topic.last_post.approved


class TopicViewTest(TestCase):
@classmethod
def setUpTestData(cls):
Expand Down Expand Up @@ -822,18 +968,6 @@ def test_breadcrumbs_on_topic_view(client, db, snapshot):
assert str(content) == snapshot(name="discussion_area_topic")


@pytest.fixture(name="topics_url")
def fixture_topics_url():
return reverse("forum_conversation_extension:topics")


@pytest.fixture(name="public_forum_with_topic")
def fixture_public_forum_with_topic(db):
forum = ForumFactory(with_public_perms=True)
TopicFactory(with_post=True, forum=forum, with_tags=["tag"])
return forum


class TestTopicListView:
def test_context(self, client, topics_url, public_forum_with_topic):
response = client.get(topics_url)
Expand Down
3 changes: 2 additions & 1 deletion lacommunaute/forum_conversation/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.urls import include, path

from lacommunaute.forum_conversation.views import TopicListView
from lacommunaute.forum_conversation.views import PostModerateView, TopicListView
from lacommunaute.forum_conversation.views_htmx import (
CertifiedPostView,
PostFeedCreateView,
Expand All @@ -18,6 +18,7 @@
path("topic/<str:slug>-<int:pk>/showmore/certified", TopicCertifiedPostView.as_view(), name="showmore_certified"),
path("topic/<str:slug>-<int:pk>/comment", PostFeedCreateView.as_view(), name="post_create"),
path("topic/<str:slug>-<int:pk>/certify", CertifiedPostView.as_view(), name="certify"),
path("topic/<str:slug>-<int:pk>/moderate", PostModerateView.as_view(), name="post_moderate"),
]


Expand Down
62 changes: 59 additions & 3 deletions lacommunaute/forum_conversation/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views.generic import ListView
from django.views.generic import ListView, View
from machina.apps.forum_conversation import views
from machina.core.loading import get_class

from lacommunaute.forum.models import Forum
from lacommunaute.forum_conversation.forms import PostForm, TopicForm
from lacommunaute.forum_conversation.models import Topic
from lacommunaute.forum_conversation.shortcuts import can_certify_post, get_posts_of_a_topic_except_first_one
from lacommunaute.forum_conversation.models import Post, Topic
from lacommunaute.forum_conversation.shortcuts import (
can_certify_post,
can_moderate_post,
get_posts_of_a_topic_except_first_one,
)
from lacommunaute.forum_conversation.view_mixins import FilteredTopicsListViewMixin
from lacommunaute.notification.models import Notification

Expand Down Expand Up @@ -78,6 +85,55 @@ def get_success_url(self):
)


class PostModerateView(View):
def dispatch(self, request, *args, **kwargs):
if request.method != "POST":
return self.http_method_not_allowed(request)

user = request.user
obj = self.get_object().topic.forum
if not (self.request.forum_permission_handler.can_read_forum(obj, user) and can_moderate_post(user)):
raise PermissionDenied

return super().dispatch(request, *args, **kwargs)

def get_object(self):
if not hasattr(self, "object"):
self.object = get_object_or_404(
Post,
pk=self.request.POST.get("post_pk", None),
)
return self.object

def post(self, request, *args, **kwargs):
post = self.get_object()
post.approved = False
post.save()
return HttpResponseRedirect(self.get_redirection_url())

def get_redirection_url(self):
messages.success(self.request, "Le message a été modéré avec succès.")
if self.object.is_topic_head:
if self.object.topic.forum.is_in_documentation_area:
return reverse(
"forum_extension:forum",
kwargs={
"slug": self.object.topic.forum.slug,
"pk": self.object.topic.forum.pk,
},
)
return reverse("forum_conversation_extension:topics")
return reverse(
"forum_conversation:topic",
kwargs={
"forum_slug": self.object.topic.forum.slug,
"forum_pk": self.object.topic.forum.pk,
"slug": self.object.topic.slug,
"pk": self.object.topic.pk,
},
)


class TopicView(views.TopicView):
def get_topic(self):
topic = super().get_topic()
Expand Down
10 changes: 10 additions & 0 deletions lacommunaute/templates/forum_conversation/partials/topic_form.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{% load i18n %}
{% load widget_tweaks %}
{% load forum_permission_tags %}
{% load permission_tags %}
<form method="post" action="." class="form" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{% for error in post_form.non_field_errors %}
<div class="alert alert-danger">
<i class="icon-exclamation-sign"></i> {{ error }}
</div>
{% endfor %}
<input type="hidden" name="post_pk" value="{{ post.pk }}">
{% include "partials/form_field.html" with field=post_form.subject %}
{% include "partials/form_field.html" with field=post_form.content %}
{% if post_form.username %}
Expand Down Expand Up @@ -98,6 +100,14 @@
<a href="{% url 'forum_conversation:post_delete' forum.slug forum.pk topic.slug topic.pk post.pk %}" aria-label="{% trans "Delete" %}" role="button" class="btn btn-outline-danger">{% trans "Delete" %}</a>
</div>
{% endif %}
{% user_can_moderate_post request.user as user_can_moderate_post %}
{% if user_can_moderate_post %}
<div class="form-group col-auto" id="moderate">
<button type="submit" formaction="{% url 'forum_conversation_extension:post_moderate' forum.slug forum.pk topic.slug topic.pk %}" class="btn btn-outline-warning">
{% trans "Moderate" %}
</button>
</div>
{% endif %}
{% endif %}
</div>
</form>
10 changes: 10 additions & 0 deletions lacommunaute/templates/forum_conversation/post_update.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% load i18n %}
{% load forum_conversation_tags %}
{% load forum_permission_tags %}
{% load permission_tags %}
{% block sub_title %}
{% trans "Edit post" %}
{% endblock sub_title %}
Expand All @@ -20,6 +21,7 @@ <h3 class="m-0 h4 card-title">{% trans "Edit post" %}</h3>
<div class="card-body">
<form method="post" action="." class="form" enctype="multipart/form-data" novalidate>
{% csrf_token %}
<input type="hidden" name="post_pk" value="{{ post.pk }}">
{% include "forum_conversation/partials/post_form.html" %}
<div class="form-actions form-row">
<div class="form-group col-auto">
Expand All @@ -31,6 +33,14 @@ <h3 class="m-0 h4 card-title">{% trans "Edit post" %}</h3>
<a href="{% url 'forum_conversation:post_delete' forum.slug forum.pk topic.slug topic.pk post.pk %}" role="button" class="btn btn-outline-danger" value="{% trans "Delete" %}">{% trans "Delete" %}</a>
</div>
{% endif %}
{% user_can_moderate_post request.user as user_can_moderate_post %}
{% if user_can_moderate_post %}
<div class="form-group col-auto" id="moderate">
<button type="submit" formaction="{% url 'forum_conversation_extension:post_moderate' forum.slug forum.pk topic.slug topic.pk %}" class="btn btn-outline-warning">
{% trans "Moderate" %}
</button>
</div>
{% endif %}
</div>
</form>
</div>
Expand Down
Loading