Skip to content
Merged
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
10 changes: 9 additions & 1 deletion frontend/templates/article_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,15 @@ <h1 class="item-title detail-title">{{ article.article_title }}</h1>
</div>
<div class="comment-body-text" id="comment-body-{{ node.comment.comment_id }}">
{% if is_deleted or node.comment.comment_written_account_id is none %}
<em>{{ _('Comment removed') }}</em>
{% if node.comment.deleted_by == 'admin' %}
<em>{{ _('Deleted by admin') }}</em>
{% elif node.comment.deleted_by == 'user' %}
<em>{{ _('Deleted by user') }}</em>
{% elif node.comment.deleted_by == 'account_deleted' %}
<em>{{ _('Deleted due to account deletion') }}</em>
{% else %}
<em>{{ _('Comment removed') }}</em>
{% endif %}
{% else %}
{{ node.comment.comment_content | safe }}
{% endif %}
Expand Down
1 change: 1 addition & 0 deletions migrations/V17__add_deleted_by_to_comments.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE comments ADD COLUMN deleted_by VARCHAR(20);
2 changes: 2 additions & 0 deletions src/application/domain/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def __init__(
is_deleted: bool = False,
deleted_at: datetime | None = None,
edited_at: datetime | None = None,
deleted_by: str | None = None,
):
self.comment_id = comment_id
self.comment_article_id = comment_article_id
Expand All @@ -41,6 +42,7 @@ def __init__(
self.is_deleted = is_deleted
self.deleted_at = deleted_at
self.edited_at = edited_at
self.deleted_by = deleted_by

@dataclass
class CommentWithAuthor:
Expand Down
2 changes: 2 additions & 0 deletions src/application/services/comment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ def mask_comments_by_account_id(self, account_id: int) -> None:
comment.comment_content = "<!--cmt-removed--><em>Comment removed</em>"
comment.is_deleted = True
comment.deleted_at = datetime.now(UTC)
comment.deleted_by = "account_deleted"
self.comment_repository.save(comment)

def delete_comment(self, comment_id: int, user_id: int) -> bool:
Expand Down Expand Up @@ -254,6 +255,7 @@ def delete_comment(self, comment_id: int, user_id: int) -> bool:

comment.is_deleted = True
comment.deleted_at = datetime.now(UTC)
comment.deleted_by = "admin" if is_admin and not is_author else "user"
self.comment_repository.save(comment)
return True

Expand Down
4 changes: 3 additions & 1 deletion src/infrastructure/input_adapters/dto/comment_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ class CommentResponse(BaseModel):
comment_posted_at: datetime | None = None
is_deleted: bool = False
edited_at: datetime | None = None
deleted_by: str | None = None

@classmethod
def from_domain(cls, comment, author_username: str = "Unknown", author_avatar_file_id: str | None = None):
"""
Helper factory to create a response DTO from a domain Comment entity.
Maps is_deleted, deleted_at, and edited_at from the domain entity.
Maps is_deleted, deleted_at, edited_at, and deleted_by from the domain entity.

If the comment's author account has been deleted (comment_written_account_id is None)
or the comment has been soft-deleted (is_deleted is True),
Expand Down Expand Up @@ -74,6 +75,7 @@ def from_domain(cls, comment, author_username: str = "Unknown", author_avatar_fi
comment_posted_at=comment.comment_posted_at,
is_deleted=is_deleted,
edited_at=comment.edited_at,
deleted_by=comment.deleted_by,
)

@classmethod
Expand Down
4 changes: 3 additions & 1 deletion src/infrastructure/output_adapters/dto/comment_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ class CommentRecord(BaseModel):
is_deleted: bool = False
deleted_at: datetime | None = None
edited_at: datetime | None = None
deleted_by: str | None = None

def to_domain(self) -> Comment:
"""
Converts the database record into a domain Comment entity.

Maps all fields including is_deleted, deleted_at, and edited_at
Maps all fields including is_deleted, deleted_at, edited_at, and deleted_by
to the domain Comment object.

Returns:
Expand All @@ -44,4 +45,5 @@ def to_domain(self) -> Comment:
is_deleted=self.is_deleted,
deleted_at=self.deleted_at,
edited_at=self.edited_at,
deleted_by=self.deleted_by,
)
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@ class CommentModel(SqlAlchemyModel):
edited_at: Mapped[datetime | None] = mapped_column(
name="edited_at", type_=TIMESTAMP, nullable=True,
)
deleted_by: Mapped[str | None] = mapped_column(
name="deleted_by", type_=String(20), nullable=True,
)
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def save(self, comment: Comment) -> None:
CommentModel.is_deleted: comment.is_deleted,
CommentModel.deleted_at: comment.deleted_at,
CommentModel.edited_at: comment.edited_at,
CommentModel.deleted_by: comment.deleted_by,
})
)
self._db_commit()
Expand All @@ -76,6 +77,7 @@ def save(self, comment: Comment) -> None:
model.is_deleted = comment.is_deleted
model.deleted_at = comment.deleted_at
model.edited_at = comment.edited_at
model.deleted_by = comment.deleted_by
self._db_commit()

def get_by_id(self, comment_id: int) -> Comment | None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def test_comment_response_from_domain_mapping():
assert response.comment_posted_at == posted_at
assert response.is_deleted is False
assert response.edited_at is None
assert response.deleted_by is None

def test_comment_response_from_domain_with_reply():
domain_comment = Comment(
Expand Down Expand Up @@ -56,6 +57,7 @@ def test_comment_response_from_domain_deleted():
assert response.author_username == "Anonymous"
assert response.comment_content == "Original content"
assert response.is_deleted is True
assert response.deleted_by is None

def test_comment_response_from_domain_edited():
posted_at = datetime(2023, 10, 27, 14, 30)
Expand Down Expand Up @@ -118,6 +120,8 @@ def test_comment_response_from_domain_with_all_fields():
assert response.author_avatar_file_id == "avatar-uuid"
assert response.is_deleted is False
assert response.edited_at is None
assert response.deleted_by is None
assert response.deleted_by is None

def test_map_nested_tree():
posted_at = datetime(2023, 10, 27, 14, 30)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ def test_to_domain_maps_all_fields_correctly(self):
assert domain.comment_reply_to == 1
assert domain.comment_content == "Mapped"
assert domain.comment_posted_at == dt
assert domain.deleted_by is None
6 changes: 4 additions & 2 deletions tests/tests_integration/test_workflow_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,12 @@ def test_comment_soft_delete_integration(self, client, db_session):
soft = db_session.get(CommentModel, rid)
assert soft.is_deleted is True
assert soft.deleted_at is not None
assert soft.deleted_by == "user"
assert soft.comment_content == "Root comment"

detail = client.get(f"/articles/{aid}")
assert b"Anonymous" in detail.data
assert b"Comment removed" in detail.data
assert b"Deleted by user" in detail.data
assert b"Root comment" not in detail.data

def test_comment_edit_and_soft_delete_integration(self, client, db_session):
Expand Down Expand Up @@ -156,12 +157,13 @@ def test_comment_edit_and_soft_delete_integration(self, client, db_session):
db_session.expire_all()
deleted = db_session.get(CommentModel, cid)
assert deleted.is_deleted is True
assert deleted.deleted_by == "user"
assert deleted.comment_content == "Updated"

detail_after_delete = client.get(f"/articles/{aid}")
assert b"Anonymous" in detail_after_delete.data
assert b"Deleted by user" in detail_after_delete.data
assert b"Updated" not in detail_after_delete.data
assert b"Comment removed" in detail_after_delete.data

def test_deep_comment_threading_integ(self, client, db_session):
"""
Expand Down
4 changes: 4 additions & 0 deletions tests/tests_services/test_comment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ def test_delete_comment_soft_delete_by_author(self):
assert comment_to_delete.is_deleted is True
assert comment_to_delete.deleted_at is not None
assert comment_to_delete.comment_content == "Original content"
assert comment_to_delete.deleted_by == "user"

def test_delete_comment_soft_delete_by_admin(self):
admin_account = create_test_account(account_id=2, account_role=AccountRole.ADMIN)
Expand All @@ -353,6 +354,7 @@ def test_delete_comment_soft_delete_by_admin(self):
self.mock_comment_repo.save.assert_called_once()
assert result is True
assert comment_to_delete.is_deleted is True
assert comment_to_delete.deleted_by == "admin"

def test_delete_comment_unauthorized_not_author(self):
fake_account = create_test_account(account_id=2, account_role=AccountRole.USER)
Expand Down Expand Up @@ -484,6 +486,8 @@ def test_mask_comments_success(self):
assert c2.comment_content == "<!--cmt-removed--><em>Comment removed</em>"
assert c1.is_deleted is True
assert c1.deleted_at is not None
assert c1.deleted_by == "account_deleted"
assert c2.deleted_by == "account_deleted"

def test_mask_comments_no_comments(self):
self.mock_comment_repo.get_by_account_id.return_value = []
Expand Down
Binary file modified translations/fr/LC_MESSAGES/messages.mo
Binary file not shown.
12 changes: 12 additions & 0 deletions translations/fr/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,18 @@ msgstr "Supprimer définitivement ce commentaire ? Action irréversible."
msgid "Comment removed"
msgstr "Commentaire supprimé."

#: frontend/templates/article_detail.html:143
msgid "Deleted by admin"
msgstr "Supprimé par l'administrateur."

#: frontend/templates/article_detail.html:143
msgid "Deleted by user"
msgstr "Supprimé par l'utilisateur."

#: frontend/templates/article_detail.html:143
msgid "Deleted due to account deletion"
msgstr "Supprimé suite à la suppression du compte."

#: frontend/templates/article_detail.html:155
#: frontend/templates/article_detail.html:227
msgid "Reply"
Expand Down
Loading