From a654d16a7d316e71e9e34175b93c7f944ff01646 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Fri, 24 Jul 2026 21:20:31 +0200 Subject: [PATCH] =?UTF-8?q?Change=20message=20delete=20comment=20:=20Added?= =?UTF-8?q?=20`deleted=5Fby`=20support,=20a=20new=20column=20and=20DTO=20f?= =?UTF-8?q?ield=20now=20record=20who=20or=20what=20triggered=20a=20comment?= =?UTF-8?q?=E2=80=99s=20soft=E2=80=91delete,=20the=20service=20sets=20?= =?UTF-8?q?=E2=80=9Cadmin=E2=80=9D,=20=E2=80=9Cuser=E2=80=9D=20or=20?= =?UTF-8?q?=E2=80=9Caccount=5Fdeleted=E2=80=9D,=20templates=20display=20th?= =?UTF-8?q?e=20specific=20reason,=20migration=20V17=20adds=20the=20column,?= =?UTF-8?q?=20all=20adapters=20handle=20the=20new=20field,=20translations?= =?UTF-8?q?=20updated=20and=20tests=20adjusted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/templates/article_detail.html | 10 +++++++++- .../V17__add_deleted_by_to_comments.sql | 1 + src/application/domain/comment.py | 2 ++ src/application/services/comment_service.py | 2 ++ .../input_adapters/dto/comment_response.py | 4 +++- .../output_adapters/dto/comment_record.py | 4 +++- .../models/sqlalchemy_comment_model.py | 3 +++ .../sqlalchemy/sqlalchemy_comment_adapter.py | 2 ++ .../dto/test_comment_response.py | 4 ++++ .../dto/test_comment_record.py | 1 + .../test_workflow_integration.py | 6 ++++-- tests/tests_services/test_comment_service.py | 4 ++++ translations/fr/LC_MESSAGES/messages.mo | Bin 12337 -> 12556 bytes translations/fr/LC_MESSAGES/messages.po | 12 ++++++++++++ 14 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 migrations/V17__add_deleted_by_to_comments.sql diff --git a/frontend/templates/article_detail.html b/frontend/templates/article_detail.html index 1efe45b4..ac0d0b6f 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 00000000..fcd3c76a --- /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 816a403a..b4506194 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 2858650e..d94bf179 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 762ea68c..a7b1978d 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 2a650b24..6c295bfc 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 d4736404..c146d480 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 984818ac..bea914d4 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 b804df43..65b64e50 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 b187f459..ee9c295f 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 58166e32..2dbb334a 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 0e6bd5c3..0eaa1b7f 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 1aa312c3ecd9be4b04dfd84bbe2202cd06fb9589..0aaf385c9031af1815f133d2902740502da8eb17 100644 GIT binary patch delta 3317 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 diff --git a/translations/fr/LC_MESSAGES/messages.po b/translations/fr/LC_MESSAGES/messages.po index c85a8908..44049082 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"