diff --git a/frontend/templates/article_detail.html b/frontend/templates/article_detail.html
index 1efe45b..ac0d0b6 100644
--- a/frontend/templates/article_detail.html
+++ b/frontend/templates/article_detail.html
@@ -140,7 +140,15 @@
{{ article.article_title }}
{% if is_deleted or node.comment.comment_written_account_id is none %}
- {{ _('Comment removed') }}
+ {% if node.comment.deleted_by == 'admin' %}
+ {{ _('Deleted by admin') }}
+ {% elif node.comment.deleted_by == 'user' %}
+ {{ _('Deleted by user') }}
+ {% elif node.comment.deleted_by == 'account_deleted' %}
+ {{ _('Deleted due to account deletion') }}
+ {% else %}
+ {{ _('Comment removed') }}
+ {% endif %}
{% else %}
{{ node.comment.comment_content | safe }}
{% endif %}
diff --git a/migrations/V17__add_deleted_by_to_comments.sql b/migrations/V17__add_deleted_by_to_comments.sql
new file mode 100644
index 0000000..fcd3c76
--- /dev/null
+++ b/migrations/V17__add_deleted_by_to_comments.sql
@@ -0,0 +1 @@
+ALTER TABLE comments ADD COLUMN deleted_by VARCHAR(20);
\ No newline at end of file
diff --git a/src/application/domain/comment.py b/src/application/domain/comment.py
index 816a403..b450619 100644
--- a/src/application/domain/comment.py
+++ b/src/application/domain/comment.py
@@ -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
@@ -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:
diff --git a/src/application/services/comment_service.py b/src/application/services/comment_service.py
index 2858650..d94bf17 100644
--- a/src/application/services/comment_service.py
+++ b/src/application/services/comment_service.py
@@ -220,6 +220,7 @@ def mask_comments_by_account_id(self, account_id: int) -> None:
comment.comment_content = "Comment removed"
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:
@@ -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
diff --git a/src/infrastructure/input_adapters/dto/comment_response.py b/src/infrastructure/input_adapters/dto/comment_response.py
index 762ea68..a7b1978 100644
--- a/src/infrastructure/input_adapters/dto/comment_response.py
+++ b/src/infrastructure/input_adapters/dto/comment_response.py
@@ -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),
@@ -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
diff --git a/src/infrastructure/output_adapters/dto/comment_record.py b/src/infrastructure/output_adapters/dto/comment_record.py
index 2a650b2..6c295bf 100644
--- a/src/infrastructure/output_adapters/dto/comment_record.py
+++ b/src/infrastructure/output_adapters/dto/comment_record.py
@@ -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:
@@ -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,
)
diff --git a/src/infrastructure/output_adapters/sqlalchemy/models/sqlalchemy_comment_model.py b/src/infrastructure/output_adapters/sqlalchemy/models/sqlalchemy_comment_model.py
index d473640..c146d48 100644
--- a/src/infrastructure/output_adapters/sqlalchemy/models/sqlalchemy_comment_model.py
+++ b/src/infrastructure/output_adapters/sqlalchemy/models/sqlalchemy_comment_model.py
@@ -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,
+ )
diff --git a/src/infrastructure/output_adapters/sqlalchemy/sqlalchemy_comment_adapter.py b/src/infrastructure/output_adapters/sqlalchemy/sqlalchemy_comment_adapter.py
index 984818a..bea914d 100644
--- a/src/infrastructure/output_adapters/sqlalchemy/sqlalchemy_comment_adapter.py
+++ b/src/infrastructure/output_adapters/sqlalchemy/sqlalchemy_comment_adapter.py
@@ -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()
@@ -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:
diff --git a/tests/tests_infrastructure/tests_input_adapters/dto/test_comment_response.py b/tests/tests_infrastructure/tests_input_adapters/dto/test_comment_response.py
index b804df4..65b64e5 100644
--- a/tests/tests_infrastructure/tests_input_adapters/dto/test_comment_response.py
+++ b/tests/tests_infrastructure/tests_input_adapters/dto/test_comment_response.py
@@ -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(
@@ -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)
@@ -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)
diff --git a/tests/tests_infrastructure/tests_output_adapters/dto/test_comment_record.py b/tests/tests_infrastructure/tests_output_adapters/dto/test_comment_record.py
index b187f45..ee9c295 100644
--- a/tests/tests_infrastructure/tests_output_adapters/dto/test_comment_record.py
+++ b/tests/tests_infrastructure/tests_output_adapters/dto/test_comment_record.py
@@ -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
diff --git a/tests/tests_integration/test_workflow_integration.py b/tests/tests_integration/test_workflow_integration.py
index 58166e3..2dbb334 100644
--- a/tests/tests_integration/test_workflow_integration.py
+++ b/tests/tests_integration/test_workflow_integration.py
@@ -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):
@@ -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):
"""
diff --git a/tests/tests_services/test_comment_service.py b/tests/tests_services/test_comment_service.py
index 0e6bd5c..0eaa1b7 100644
--- a/tests/tests_services/test_comment_service.py
+++ b/tests/tests_services/test_comment_service.py
@@ -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)
@@ -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)
@@ -484,6 +486,8 @@ def test_mask_comments_success(self):
assert c2.comment_content == "Comment removed"
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 = []
diff --git a/translations/fr/LC_MESSAGES/messages.mo b/translations/fr/LC_MESSAGES/messages.mo
index 1aa312c..0aaf385 100644
Binary files a/translations/fr/LC_MESSAGES/messages.mo and b/translations/fr/LC_MESSAGES/messages.mo differ
diff --git a/translations/fr/LC_MESSAGES/messages.po b/translations/fr/LC_MESSAGES/messages.po
index c85a890..4404908 100644
--- a/translations/fr/LC_MESSAGES/messages.po
+++ b/translations/fr/LC_MESSAGES/messages.po
@@ -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"