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"