Skip to content

Commit

Permalink
feat(article): can edit & delete comments
Browse files Browse the repository at this point in the history
Close #282
  • Loading branch information
Jenselme committed Oct 19, 2024
1 parent c56f1ac commit 08dd2ee
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 18 deletions.
10 changes: 9 additions & 1 deletion legadilo/reading/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from legadilo.users.tests.factories import UserFactory

from ..models import Article, ArticleFetchError, ReadingList, Tag
from ..models import Article, ArticleFetchError, Comment, ReadingList, Tag


class ArticleFactory(DjangoModelFactory):
Expand Down Expand Up @@ -67,3 +67,11 @@ class ArticleFetchErrorFactory(DjangoModelFactory):

class Meta:
model = ArticleFetchError


class CommentFactory(DjangoModelFactory):
text = factory.Sequence(lambda n: f"Comment {n}")
article = factory.SubFactory(ArticleFactory)

class Meta:
model = Comment
85 changes: 84 additions & 1 deletion legadilo/reading/tests/test_views/test_comment_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
from django.urls import reverse

from legadilo.conftest import assert_redirected_to_login_page
from legadilo.reading.models import Article
from legadilo.reading.models import Article, Comment
from legadilo.reading.tests.factories import CommentFactory

COMMENT = """My comment
with <em>nasty HTML</em>
Expand Down Expand Up @@ -77,3 +78,85 @@ def test_create(self, logged_in_sync_client, user, django_assert_num_queries):
assert article.comments.count() == 1
comment = article.comments.get()
assert comment.text == CLEANED_COMMENT


@pytest.mark.django_db
class TestDisplayCommentView:
@pytest.fixture(autouse=True)
def _setup_data(self, user):
self.comment = CommentFactory(article__user=user)
self.url = reverse("reading:display_comment", kwargs={"pk": self.comment.id})

def test_not_logged_in(self, client):
response = client.get(self.url)

assert_redirected_to_login_page(response)

def test_display_invalid_user(self, logged_in_other_user_sync_client):
response = logged_in_other_user_sync_client.get(self.url)

assert response.status_code == HTTPStatus.NOT_FOUND

def test_display(self, logged_in_sync_client, django_assert_num_queries):
with django_assert_num_queries(6):
response = logged_in_sync_client.get(self.url)

assert response.status_code == HTTPStatus.OK


@pytest.mark.django_db
class TestEditCommentView:
@pytest.fixture(autouse=True)
def _setup_data(self, user):
self.comment = CommentFactory(article__user=user)
self.url = reverse("reading:edit_comment", kwargs={"pk": self.comment.id})

def test_not_logged_in(self, client):
response = client.get(self.url)

assert_redirected_to_login_page(response)

def test_edit_invalid_user(self, logged_in_other_user_sync_client):
response = logged_in_other_user_sync_client.post(self.url)

assert response.status_code == HTTPStatus.NOT_FOUND

def test_get_edit_form(self, logged_in_sync_client, django_assert_num_queries):
with django_assert_num_queries(6):
response = logged_in_sync_client.get(self.url)

assert response.status_code == HTTPStatus.OK
assert response.context_data["comment"] == self.comment

def test_edit(self, logged_in_sync_client, django_assert_num_queries):
with django_assert_num_queries(7):
response = logged_in_sync_client.post(self.url, data={"text": "Updated text"})

assert response.status_code == HTTPStatus.OK
self.comment.refresh_from_db()
assert self.comment.text == "Updated text"


@pytest.mark.django_db
class TestDeleteCommentView:
@pytest.fixture(autouse=True)
def _setup_data(self, user):
self.comment = CommentFactory(article__user=user)
self.url = reverse("reading:delete_comment", kwargs={"pk": self.comment.id})

def test_not_logged_in(self, client):
response = client.post(self.url)

assert_redirected_to_login_page(response)

def test_edit_invalid_user(self, logged_in_other_user_sync_client):
response = logged_in_other_user_sync_client.post(self.url)

assert response.status_code == HTTPStatus.NOT_FOUND

def test_delete(self, logged_in_sync_client, django_assert_num_queries):
with django_assert_num_queries(7):
response = logged_in_sync_client.post(self.url)

assert response.status_code == HTTPStatus.OK
assert Comment.objects.count() == 0
3 changes: 3 additions & 0 deletions legadilo/reading/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,7 @@
),
path("search/", views.search_view, name="search"),
path("comment/", views.create_comment_view, name="create_comment"),
path("comment/<int:pk>/", views.display_comment_view, name="display_comment"),
path("comment/<int:pk>/edit/", views.edit_comment_view, name="edit_comment"),
path("comment/<int:pk>/delete/", views.delete_comment_view, name="delete_comment"),
]
10 changes: 9 additions & 1 deletion legadilo/reading/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
update_article_view,
)
from .article_details_views import article_details_view
from .comment_views import create_comment_view
from .comment_views import (
create_comment_view,
delete_comment_view,
display_comment_view,
edit_comment_view,
)
from .fetch_article_views import add_article_view, refetch_article_view
from .list_of_articles_views import (
external_tag_with_articles_view,
Expand All @@ -38,6 +43,9 @@
"article_details_view",
"create_comment_view",
"create_tag_view",
"delete_comment_view",
"display_comment_view",
"edit_comment_view",
"edit_tag_view",
"external_tag_with_articles_view",
"mark_articles_as_read_in_bulk_view",
Expand Down
59 changes: 54 additions & 5 deletions legadilo/reading/views/comment_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@
from django import forms
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
from django.http import HttpResponseBadRequest
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST
from django.views.decorators.http import require_GET, require_http_methods, require_POST

from legadilo.reading.models import Article, Comment
from legadilo.users.user_types import AuthenticatedHttpRequest
from legadilo.utils.security import full_sanitize


class CommentArticleForm(forms.Form):
article_id = forms.IntegerField(required=True, widget=forms.HiddenInput)
class CommentArticleForm(forms.ModelForm):
article_id = forms.IntegerField(required=False, widget=forms.HiddenInput)
text = forms.CharField(
required=True,
widget=forms.Textarea(
Expand All @@ -42,6 +42,7 @@ class CommentArticleForm(forms.Form):

class Meta:
fields = ("text",)
model = Comment

def clean_text(self):
text = self.cleaned_data.get("text", "")
Expand All @@ -64,4 +65,52 @@ def create_comment_view(
article = get_object_or_404(Article, id=form.cleaned_data["article_id"], user=request.user)
comment = Comment.objects.create(article=article, text=form.cleaned_data["text"])

return TemplateResponse(request, "reading/partials/comment.html", {"comment": comment})
return TemplateResponse(
request, "reading/partials/comment.html#comment-card", {"comment": comment}
)


@require_GET
@login_required
def display_comment_view(request: AuthenticatedHttpRequest, pk: int) -> TemplateResponse:
comment = get_object_or_404(Comment, pk=pk, article__user=request.user)

return TemplateResponse(
request, "reading/partials/comment.html#comment-card", {"comment": comment}
)


@require_http_methods(["GET", "POST"])
@login_required
def edit_comment_view(request: AuthenticatedHttpRequest, pk: int) -> TemplateResponse:
comment = get_object_or_404(Comment, pk=pk, article__user=request.user)
form = CommentArticleForm(
initial={"article_id": comment.article_id, "text": comment.text}, instance=comment
)

if request.method == "POST":
form = CommentArticleForm(request.POST, instance=comment)
if form.is_valid():
form.save()
return TemplateResponse(
request, "reading/partials/comment.html#comment-card", {"comment": comment}
)

return TemplateResponse(
request,
"reading/partials/comment.html#edit-comment-form",
{
"comment": comment,
"comment_article_form": form,
},
)


@require_POST
@login_required
def delete_comment_view(request: AuthenticatedHttpRequest, pk: int) -> HttpResponse:
comment = get_object_or_404(Comment, pk=pk, article__user=request.user)

comment.delete()

return HttpResponse()
2 changes: 1 addition & 1 deletion legadilo/templates/reading/article_details.html
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ <h1 id="legadilo-article-title">{{ article.title |safe }}</h1>
</form>
<div id="all-comments" class="mt-3">
{% for comment in article.comments.all %}
{% include "reading/partials/comment.html" with comment=comment %}
{% include "reading/partials/comment.html#comment-card" with comment=comment %}
{% endfor %}
</div>
</div>
Expand Down
46 changes: 37 additions & 9 deletions legadilo/templates/reading/partials/comment.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
{% load i18n %}
{% load i18n crispy_forms_tags partials %}

<div id="comment-{{ comment.id }}" class="card mt-2">
<div class="card-body">
<p class="lead">
{% partialdef comment-card %}
<div class="card mt-2" hx-target="this" hx-swap="outerHTML">
<div class="card-header">
{% blocktranslate with comment_date=comment.created_at|date:"SHORT_DATETIME_FORMAT" %}
You commented on {{ comment_date }}
{% endblocktranslate %}
</p>
<div class="card-text">{{ comment.text|markdown }}</div>
You commented on {{ comment_date }}
{% endblocktranslate %}
</div>
<div class="card-body">
<div class="card-text">{{ comment.text|markdown }}</div>
</div>
<div class="card-footer">
<button class="btn btn-sm btn-primary"
type="button"
hx-get="{% url 'reading:edit_comment' pk=comment.id %}">{% translate "Edit" %}</button>
<form class="d-inline"
data-modal-id="danger-modal"
data-modal-title="{% translate 'Confirm deletion' %}"
data-modal-body="{% translate 'Do you really want to delete this comment?' %}"
hx-post="{% url 'reading:delete_comment' pk=comment.id %}"
hx-confirm="Delete?">
{% csrf_token %}
<button class="btn btn-sm btn-danger">{% translate "Delete" %}</button>
</form>
</div>
</div>
</div>
{% endpartialdef %}
{% partialdef edit-comment-form %}
<form hx-post="{% url 'reading:edit_comment' pk=comment.id %}"
hx-target="this"
hx-swap="outerHTML">
{% csrf_token %}
{{ comment_article_form|crispy }}
<button class="btn btn-sm btn-outline-primary" type="submit">{% translate "Save" %}</button>
<button class="btn btn-sm btn-outline-danger"
type="button"
hx-get="{% url 'reading:display_comment' pk=comment.id %}">{% translate "Cancel" %}</button>
</form>
{% endpartialdef %}

0 comments on commit 08dd2ee

Please sign in to comment.