Skip to content

Commit

Permalink
feat: add audit logging for BaseCommentViewSet
Browse files Browse the repository at this point in the history
Audit logged functionality:
- (un)vote
- flag
- create
- update
- delete
- retrieve
- list

Refs: KER-368
  • Loading branch information
charn committed Aug 27, 2024
1 parent 252d1e3 commit c09d2d9
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 1 deletion.
78 changes: 78 additions & 0 deletions democracy/tests/integrationtest/test_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
from django.utils.timezone import now
from rest_framework import status
from reversion.models import Version
from urllib.parse import urlparse

from audit_log.enums import Operation
from democracy.enums import Commenting, InitialSectionType
from democracy.factories.hearing import SectionCommentFactory
from democracy.factories.poll import SectionPollFactory
from democracy.models import Hearing, Label, Section, SectionType
from democracy.models.section import SectionComment, SectionPoll, SectionPollAnswer
from democracy.tests.conftest import default_comment_content, default_lang_code
from democracy.tests.utils import (
assert_audit_log_entry,
assert_common_keys_equal,
get_data_from_response,
get_hearing_detail_url,
Expand Down Expand Up @@ -1684,3 +1687,78 @@ def test_hearing_sections_comment_num_queries(
with django_assert_num_queries(7):
response = john_doe_api_client.get(url)
get_data_from_response(response, 200)


@pytest.mark.django_db
def test_comment_id_is_audit_logged_on_flag(john_smith_api_client, default_hearing, audit_log_configure):
section = default_hearing.sections.first()
comment = section.comments.first()
url = get_section_comment_flag_url(default_hearing.id, section.id, comment.id)

john_smith_api_client.post(url)

assert_audit_log_entry("/flag", [comment.pk])


@pytest.mark.django_db
def test_comment_id_is_audit_logged_on_create(
john_doe_api_client, default_hearing, get_comments_url_and_data, audit_log_configure
):
section = default_hearing.sections.first()
url, data = get_comments_url_and_data(default_hearing, section)

response = john_doe_api_client.post(url, data=data)
data = get_data_from_response(response, 201)

assert_audit_log_entry(urlparse(url).path, [data["id"]])


@pytest.mark.django_db
def test_comment_id_is_audit_logged_on_edit(john_doe_api_client, default_hearing, get_detail_url, audit_log_configure):
section = default_hearing.get_main_section()
comment = section.comments.all()[0]
url = get_detail_url(comment)

john_doe_api_client.patch(url, data={"content": "B"})

assert_audit_log_entry(url, [comment.pk], operation=Operation.UPDATE)


@pytest.mark.django_db
def test_comment_id_is_audit_logged_on_delete(
john_doe_api_client, default_hearing, get_detail_url, audit_log_configure
):
section = default_hearing.get_main_section()
comment = section.comments.all()[0]
url = get_detail_url(comment)

john_doe_api_client.delete(url)

assert_audit_log_entry(url, [comment.pk], operation=Operation.DELETE)


@pytest.mark.django_db
def test_comment_id_is_audit_logged_on_retrieve(
john_doe_api_client, default_hearing, get_detail_url, audit_log_configure
):
section = default_hearing.get_main_section()
comment = section.comments.all()[0]
url = get_detail_url(comment)

john_doe_api_client.get(url)

assert_audit_log_entry(url, [comment.pk], operation=Operation.READ)


@pytest.mark.django_db
def test_comment_ids_are_audit_logged_on_list(
john_doe_api_client, default_hearing, get_comments_url_and_data, audit_log_configure
):
section = default_hearing.get_main_section()
url, data = get_comments_url_and_data(default_hearing, section)
comments = section.comments.all()
assert comments.count() > 1

john_doe_api_client.get(url)

assert_audit_log_entry(urlparse(url).path, comments.values_list("pk", flat=True), operation=Operation.READ)
20 changes: 20 additions & 0 deletions democracy/tests/integrationtest/test_comment_vote.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from democracy.enums import Commenting, InitialSectionType
from democracy.models import Section, SectionComment, SectionType
from democracy.tests.integrationtest.test_images import get_hearing_detail_url
from democracy.tests.utils import assert_audit_log_entry

default_content = "Awesome comment to vote."
comment_data = {"content": default_content, "section": None}
Expand Down Expand Up @@ -125,3 +126,22 @@ def test_vote_appears_in_user_data(john_doe_api_client, default_hearing):
john_doe_api_client.post(get_section_comment_vote_url(default_hearing.id, section.id, sc_comment.id))
response = john_doe_api_client.get("/v1/users/")
assert sc_comment.id in response.data[0]["voted_section_comments"]


@pytest.mark.django_db
def test_comment_id_is_audit_logged_on_vote(john_doe_api_client, default_hearing, audit_log_configure):
section, comment = add_default_section_and_comment(default_hearing)

john_doe_api_client.post(get_section_comment_vote_url(default_hearing.id, section.id, comment.id))

assert_audit_log_entry("/vote", [comment.pk])


@pytest.mark.django_db
def test_comment_id_is_audit_logged_on_unvote(api_client, john_doe_api_client, default_hearing, audit_log_configure):
section, comment = add_default_section_and_comment(default_hearing)

john_doe_api_client.post(get_section_comment_vote_url(default_hearing.id, section.id, comment.id))
john_doe_api_client.post(get_section_comment_unvote_url(default_hearing.id, section.id, comment.id))

assert_audit_log_entry("/unvote", [comment.pk], 2)
18 changes: 18 additions & 0 deletions democracy/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import json
import os
from collections import Counter
from django.utils.dateparse import parse_datetime
from io import BytesIO
from PIL import Image
from typing import Iterable, Mapping

from audit_log.enums import Operation, Status
from audit_log.models import AuditLogEntry
from democracy.models.files import BaseFile
from democracy.models.images import BaseImage
from democracy.utils.file_to_base64 import file_to_base64
Expand Down Expand Up @@ -215,3 +218,18 @@ def get_nested(data: Mapping, keys: Iterable):
def instance_ids(instances: Iterable) -> set:
"""Get a set of unique IDs from an iterable of model instances, e.g. a list, queryset."""
return {instance.id for instance in instances}


def assert_audit_log_entry(
path: str,
object_ids: list,
count: int = 1,
status: Status = Status.SUCCESS,
operation: Operation = Operation.CREATE,
):
assert AuditLogEntry.objects.count() == count
audit_log_entry = AuditLogEntry.objects.order_by("-created_at").first()
assert path in audit_log_entry.message["audit_event"]["target"]["path"]
assert Counter(audit_log_entry.message["audit_event"]["target"]["object_ids"]) == Counter(object_ids)
assert audit_log_entry.message["audit_event"]["status"] == status.value
assert audit_log_entry.message["audit_event"]["operation"] == operation.value
9 changes: 8 additions & 1 deletion democracy/views/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from rest_framework.settings import api_settings
from reversion.views import RevisionMixin

from audit_log.utils import add_audit_logged_object_ids
from audit_log.views import AuditLogApiView
from democracy.models.comment import BaseComment
from democracy.renderers import GeoJSONRenderer
from democracy.views.base import AdminsSeeUnpublishedMixin, CreatedBySerializer
Expand Down Expand Up @@ -80,7 +82,7 @@ class Meta:
]


class BaseCommentViewSet(AdminsSeeUnpublishedMixin, RevisionMixin, viewsets.ModelViewSet):
class BaseCommentViewSet(AdminsSeeUnpublishedMixin, RevisionMixin, AuditLogApiView, viewsets.ModelViewSet):
"""
Base viewset for comments.
"""
Expand Down Expand Up @@ -190,6 +192,7 @@ def create(self, request, *args, **kwargs):
if self.request.user.is_authenticated:
kwargs["created_by"] = self.request.user
comment = serializer.save(**kwargs)
add_audit_logged_object_ids(self.request, serializer.instance)
reversion.set_comment("Comment created")
# and another for the response
serializer = self.get_serializer(instance=comment)
Expand Down Expand Up @@ -258,6 +261,7 @@ def destroy(self, request, *args, **kwargs):
)

instance.soft_delete(user=request.user)
add_audit_logged_object_ids(self.request, instance)
reversion.set_comment("Comment deleted")

return response.Response(status=status.HTTP_204_NO_CONTENT)
Expand All @@ -279,6 +283,7 @@ def vote(self, request, **kwargs):
return response.Response({"status": "Already voted"}, status=status.HTTP_304_NOT_MODIFIED)
# add voter
comment.voters.add(request.user)
add_audit_logged_object_ids(self.request, comment)
# update number of votes
comment.recache_n_votes()
# return success
Expand All @@ -299,6 +304,7 @@ def flag(self, request, **kwargs):
instance.flagged_at = timezone.now()
instance.flagged_by = request.user
instance.save()
add_audit_logged_object_ids(self.request, instance)
return response.Response({"status": "comment flagged"})

@action(detail=True, methods=["post"])
Expand All @@ -313,6 +319,7 @@ def unvote(self, request, **kwargs):
if comment.__class__.objects.filter(id=comment.id, voters=request.user).exists():
# remove voter
comment.voters.remove(request.user)
add_audit_logged_object_ids(self.request, comment)
# update number of votes
comment.recache_n_votes()
# return success
Expand Down
5 changes: 5 additions & 0 deletions kerrokantasi/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ def pytest_configure():
)


@pytest.fixture
def audit_log_configure(settings):
settings.AUDIT_LOG = {"ENABLED": True}


@pytest.fixture(autouse=True)
def setup_test_media(settings):
"""Create folder for test media/file uploads."""
Expand Down

0 comments on commit c09d2d9

Please sign in to comment.