Fakultas Ilmu Komputer UI

Commit ecb65aa4 authored by Jonathan Christopher Jakub's avatar Jonathan Christopher Jakub
Browse files

Implement Activity Log

parent 1faf6b8e
from django_filters import FilterSet, CharFilter
from apps.accounts.models import (
Account,
)
from apps.accounts.models import Account
class AccountFilter(FilterSet):
......
......@@ -43,6 +43,6 @@ class Migration(migrations.Migration):
),
),
],
options={"verbose_name_plural": "accounts", "db_table": "account", },
options={"verbose_name_plural": "accounts", "db_table": "account",},
),
]
......@@ -7,12 +7,19 @@ from rest_framework.test import APITestCase, APIClient
from apps.accounts.models import Account
from apps.accounts.tests.factories.accounts import AccountFactory, UserFactory
from apps.constants import HEADER_PREFIX
from apps.constants import (
HEADER_PREFIX,
ACTIVITY_TYPE_CREATE,
ACTIVITY_TYPE_EDIT,
ACTIVITY_TYPE_DELETE,
)
class AccountViewTest(APITestCase):
@classmethod
def setUpTestData(self):
self.BASE_URL = "/accounts/"
self.user_1 = UserFactory(username="user_1", password="justpass")
self.user_2 = UserFactory(username="user_2", password="justpass")
self.admin = AccountFactory(admin=True, user=self.user_1)
......@@ -25,12 +32,10 @@ class AccountViewTest(APITestCase):
self.faker = Faker()
def setUp(self):
self.client = APIClient(
HTTP_AUTHORIZATION=HEADER_PREFIX + self.token_1.key
)
self.client = APIClient(HTTP_AUTHORIZATION=HEADER_PREFIX + self.token_1.key)
def test_list_all_accounts_success(self):
url = "/accounts/"
url = self.BASE_URL
response = self.client.get(url)
response_string = response.rendered_content.decode("utf-8")
......@@ -39,7 +44,7 @@ class AccountViewTest(APITestCase):
self.assertIn('"user_2"', response_string)
def test_list_all_accounts_paginate_failed(self):
url = "/accounts/?page=100"
url = self.BASE_URL + "?page=100"
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
......@@ -49,7 +54,7 @@ class AccountViewTest(APITestCase):
self.assertIn('"detail":"Invalid page."', response_string)
def test_list_all_accounts_filter_success(self):
url = "/accounts/?username=" + self.user_1.username
url = self.BASE_URL + "?username=" + self.user_1.username
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
......@@ -59,7 +64,7 @@ class AccountViewTest(APITestCase):
self.assertIn('"count":1', response_string)
def test_list_all_accounts_filter_failed(self):
url = "/accounts/?username=1234567890"
url = self.BASE_URL + "?username=1234567890"
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
......@@ -69,7 +74,7 @@ class AccountViewTest(APITestCase):
self.assertIn('"count":0', response_string)
def test_retrieve_account_success(self):
url = "/accounts/" + str(self.officer.id) + "/"
url = self.BASE_URL + str(self.officer.id) + "/"
response = self.client.get(url)
data = {
"id": str(self.officer.id),
......@@ -87,7 +92,7 @@ class AccountViewTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_new_admin_success(self):
url = "/accounts/"
url = self.BASE_URL
_account_id = self.faker.email()
admin_prev_count = Account.objects.filter(is_admin=True).count()
......@@ -109,7 +114,7 @@ class AccountViewTest(APITestCase):
self.assertEqual(admin_current_count, admin_prev_count + 1)
def test_create_new_officer_success(self):
url = "/accounts/"
url = self.BASE_URL
_account_id = self.faker.email()
officer_prev_count = Account.objects.filter(is_admin=False).count()
......@@ -130,8 +135,18 @@ class AccountViewTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(officer_current_count, officer_prev_count + 1)
# Have account creation log for the new officer
new_officer_id = Account.objects.filter(email=_account_id)[0].id
response = self.client.get("/logs/")
response_string = response.rendered_content.decode("utf-8")
self.assertIn('"object_id":"{}"'.format(new_officer_id), response_string)
self.assertIn(
'"action_type":"{}"'.format(ACTIVITY_TYPE_CREATE), response_string
)
def test_create_new_account_fails_with_no_auth_info(self):
url = "/accounts/"
url = self.BASE_URL
_account_id = self.faker.email()
......@@ -147,7 +162,7 @@ class AccountViewTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_edit_account_success_by(self):
url = "/accounts/" + str(self.officer.id) + "/"
url = self.BASE_URL + str(self.officer.id) + "/"
data = {
"id": str(self.officer.id),
......@@ -160,9 +175,7 @@ class AccountViewTest(APITestCase):
"is_active": True,
}
self.client = APIClient(
HTTP_AUTHORIZATION=HEADER_PREFIX + self.token_2.key
)
self.client = APIClient(HTTP_AUTHORIZATION=HEADER_PREFIX + self.token_2.key)
response = self.client.put(path=url, data=data, format="json",)
expected_returned_data = data
......@@ -173,8 +186,15 @@ class AccountViewTest(APITestCase):
str(response.content, encoding="utf8"), expected_returned_data
)
# Have account update log
response = self.client.get("/logs/")
response_string = response.rendered_content.decode("utf-8")
self.assertIn('"object_id":"{}"'.format(self.officer.id), response_string)
self.assertIn('"action_type":"{}"'.format(ACTIVITY_TYPE_EDIT), response_string)
def test_edit_account_fail_without_complete_fields(self):
url = "/accounts/" + str(self.officer.id) + "/"
url = self.BASE_URL + str(self.officer.id) + "/"
data = {
"id": str(self.officer.id),
......@@ -186,8 +206,17 @@ class AccountViewTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_delete_success(self):
url = "/accounts/" + str(self.officer.id) + "/"
url = self.BASE_URL + str(self.officer.id) + "/"
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Have account deletion log
response = self.client.get("/logs/")
response_string = response.rendered_content.decode("utf-8")
self.assertIn('"object_id":"{}"'.format(self.officer.id), response_string)
self.assertIn(
'"action_type":"{}"'.format(ACTIVITY_TYPE_DELETE), response_string
)
"""app URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
......
......@@ -15,10 +15,17 @@ from apps.commons.permissions import (
IsSelfOrAdministrator,
CreateOnly,
)
from apps.logs.models import Log
from apps.constants import (
MODEL_NAME_ACCOUNT,
ACTIVITY_TYPE_CREATE,
ACTIVITY_TYPE_EDIT,
ACTIVITY_TYPE_DELETE,
)
class AccountViewSet(viewsets.ViewSet):
queryset = Account.objects.all().select_related("user")
queryset = Account.objects.all().select_related("user").order_by("-created_at")
filter_backends = (DjangoFilterBackend,)
permission_classes = [
IsSelfOrAdministrator | CreateOnly,
......@@ -48,6 +55,14 @@ class AccountViewSet(viewsets.ViewSet):
account = Account.objects.create(user=user, **serializer.validated_data)
# Add account creation log
Log.objects.create(
model_name=MODEL_NAME_ACCOUNT,
object_id=account.id,
action_type=ACTIVITY_TYPE_CREATE,
author=request.user.account,
)
return Response(
AccountSerializer(account).data, status=status.HTTP_201_CREATED,
)
......@@ -59,11 +74,28 @@ class AccountViewSet(viewsets.ViewSet):
serializer.is_valid(raise_exception=True)
serializer.save()
# Add account update log
Log.objects.create(
model_name=MODEL_NAME_ACCOUNT,
object_id=pk,
action_type=ACTIVITY_TYPE_EDIT,
author=request.user.account,
)
return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, pk=None):
instance = get_object_or_404(self.queryset, pk=pk)
self.check_object_permissions(request, instance)
instance.delete()
# Add account deletion log
Log.objects.create(
model_name=MODEL_NAME_ACCOUNT,
object_id=pk,
action_type=ACTIVITY_TYPE_DELETE,
author=request.user.account,
)
serializer = AccountSerializer(instance=instance)
return Response(serializer.data, status=status.HTTP_200_OK)
......@@ -11,7 +11,7 @@ from apps.constants import TIMEZONE
class CaseSubject(models.Model):
revision_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
subject_id = models.UUIDField(default=uuid.uuid4)
subject_id = models.UUIDField(default=uuid.uuid4, editable=False)
name = models.CharField(max_length=128)
age = models.IntegerField()
is_male = models.BooleanField(db_index=True)
......@@ -38,7 +38,7 @@ class CaseSubject(models.Model):
class PositiveCase(models.Model):
revision_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
case_id = models.UUIDField(default=uuid.uuid4)
case_id = models.UUIDField(default=uuid.uuid4, editable=False)
case_subject_id = models.UUIDField(blank=True, null=True)
outcome = models.CharField(max_length=256)
author = models.ForeignKey(
......@@ -74,7 +74,7 @@ class PositiveCase(models.Model):
class InvestigationCase(models.Model):
revision_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
case_id = models.UUIDField(default=uuid.uuid4)
case_id = models.UUIDField(default=uuid.uuid4, editable=False)
case_subject_id = models.UUIDField(blank=True, null=True)
reference_case_id = models.UUIDField(blank=True, null=True)
case_relation = models.CharField(max_length=128, blank=True)
......@@ -121,7 +121,7 @@ class InvestigationCase(models.Model):
class MonitoringCase(models.Model):
revision_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
case_id = models.UUIDField(default=uuid.uuid4)
case_id = models.UUIDField(default=uuid.uuid4, editable=False)
investigation_case_id = models.UUIDField(blank=True, null=True)
positive_case_id = models.UUIDField(blank=True, null=True)
is_referred = models.BooleanField(blank=True, null=True, db_index=True)
......
from rest_framework import serializers
from apps.accounts.serializers import AccountSerializer
from apps.cases.models import (
CaseSubject,
InvestigationCase,
......@@ -35,7 +37,6 @@ class PositiveCaseSerializer(serializers.ModelSerializer):
"case_id",
"case_subject_id",
"outcome",
"author",
"is_active",
]
......@@ -43,12 +44,14 @@ class PositiveCaseSerializer(serializers.ModelSerializer):
class PositiveCaseSummarySerializer(serializers.ModelSerializer):
case_subject = serializers.SerializerMethodField()
is_active = serializers.BooleanField(read_only=True)
author = AccountSerializer()
class Meta:
model = PositiveCase
fields = [
"revision_id",
"case_id",
"case_subject_id",
"case_subject",
"outcome",
"author",
......@@ -76,7 +79,6 @@ class InvestigationCaseSerializer(serializers.ModelSerializer):
"risk_factors",
"is_referral_needed",
"medical_facility_reference",
"author",
"is_active",
]
......@@ -85,13 +87,16 @@ class InvestigationCaseSummarySerializer(serializers.ModelSerializer):
case_subject = serializers.SerializerMethodField()
reference_case = serializers.SerializerMethodField()
is_active = serializers.BooleanField(read_only=True)
author = AccountSerializer()
class Meta:
model = InvestigationCase
fields = [
"revision_id",
"case_id",
"case_subject_id",
"case_subject",
"reference_case_id",
"reference_case",
"case_relation",
"medical_symptoms",
......@@ -129,7 +134,6 @@ class MonitoringCaseSerializer(serializers.ModelSerializer):
"treatment_start_date",
"treatment_end_date",
"outcome",
"author",
"is_active",
]
......@@ -138,13 +142,16 @@ class MonitoringCaseSummarySerializer(serializers.ModelSerializer):
investigation_case = serializers.SerializerMethodField()
positive_case = serializers.SerializerMethodField()
is_active = serializers.BooleanField(read_only=True)
author = AccountSerializer()
class Meta:
model = MonitoringCase
fields = [
"revision_id",
"case_id",
"investigation_case_id",
"investigation_case",
"positive_case_id",
"positive_case",
"is_referred",
"is_checked",
......
......@@ -8,13 +8,21 @@ from rest_framework.test import APITestCase, APIClient
from apps.accounts.tests.factories.accounts import AccountFactory, UserFactory
from apps.cases.models import CaseSubject
from apps.cases.tests.factories.case_subjects import CaseSubjectFactory
from apps.constants import HEADER_PREFIX
from apps.constants import TIMEZONE
from apps.constants import (
HEADER_PREFIX,
TIMEZONE,
ACTIVITY_TYPE_CREATE,
ACTIVITY_TYPE_EDIT,
ACTIVITY_TYPE_DELETE,
)
class CaseSubjectViewTest(APITestCase):
@classmethod
def setUpTestData(self):
self.BASE_URL = "/cases/case-subjects/"
self.user_1 = UserFactory(username="user_1", password="justpass")
self.user_2 = UserFactory(username="user_2", password="justpass")
self.admin = AccountFactory(admin=True, user=self.user_1)
......@@ -31,7 +39,7 @@ class CaseSubjectViewTest(APITestCase):
self.client = APIClient(HTTP_AUTHORIZATION=HEADER_PREFIX + self.token_1.key)
def test_list_case_subjects_only_shows_active_entries_success(self):
url = "/cases/case-subjects/"
url = self.BASE_URL
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
......@@ -56,7 +64,7 @@ class CaseSubjectViewTest(APITestCase):
self.assertJSONEqual(json.dumps(response.data), data)
def test_list_case_subjects_paginate_failed(self):
url = "/cases/case-subjects/?page=100"
url = self.BASE_URL + "?page=100"
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
......@@ -66,7 +74,7 @@ class CaseSubjectViewTest(APITestCase):
self.assertIn('"detail":"Invalid page."', response_string)
def test_list_case_subjects_filter_success(self):
url = "/cases/case-subjects/?district=" + self.case_subject_1.district
url = self.BASE_URL + "?district=" + self.case_subject_1.district
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
......@@ -76,7 +84,7 @@ class CaseSubjectViewTest(APITestCase):
self.assertIn('"count":1', response_string)
def test_list_case_subjects_filter_failed(self):
url = "/cases/case-subjects/?district=1234567890"
url = self.BASE_URL + "?district=1234567890"
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
......@@ -86,7 +94,7 @@ class CaseSubjectViewTest(APITestCase):
self.assertIn('"count":0', response_string)
def test_retrieve_case_subject_success(self):
url = "/cases/case-subjects/" + str(self.case_subject_1.revision_id) + "/"
url = self.BASE_URL + str(self.case_subject_1.revision_id) + "/"
response = self.client.get(url)
......@@ -106,12 +114,12 @@ class CaseSubjectViewTest(APITestCase):
self.assertJSONEqual(json.dumps(response.data), data)
def test_retrieve_case_subject_fails_on_deleted_subject(self):
url = "/cases/case-subjects/" + str(self.case_subject_2.revision_id) + "/"
url = self.BASE_URL + str(self.case_subject_2.revision_id) + "/"
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_create_new_case_subject_success(self):
url = "/cases/case-subjects/"
url = self.BASE_URL
case_subject_prev_active_count = CaseSubject.objects.active_revisions().count()
case_subject_prev_all_count = CaseSubject.objects.all().count()
......@@ -141,8 +149,22 @@ class CaseSubjectViewTest(APITestCase):
case_subject_current_all_count, case_subject_prev_all_count + 1
)
# Have case subject creation log
logs_response = self.client.get("/logs/")
response_string = logs_response.rendered_content.decode("utf-8")
self.assertIn(
'"object_id":"{}"'.format(response.data["subject_id"]), response_string
)
self.assertIn(
'"revision_id":"{}"'.format(response.data["revision_id"]), response_string
)
self.assertIn(
'"action_type":"{}"'.format(ACTIVITY_TYPE_CREATE), response_string
)
def test_create_new_case_subject_fails_with_incomplete_data(self):
url = "/cases/case-subjects/"
url = self.BASE_URL
data = {
"is_male": self.case_subject_1.is_male,
......@@ -155,7 +177,7 @@ class CaseSubjectViewTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_edit_case_subject_success(self):
url = "/cases/case-subjects/" + str(self.case_subject_1.revision_id) + "/"
url = self.BASE_URL + str(self.case_subject_1.revision_id) + "/"
data = {
"subject_id": str(self.case_subject_1.subject_id),
......@@ -191,8 +213,21 @@ class CaseSubjectViewTest(APITestCase):
prev_active_case_subject_revision_count,
)
# Have case subject update log
response = self.client.get("/logs/")
response_string = response.rendered_content.decode("utf-8")
self.assertIn(
'"object_id":"{}"'.format(self.case_subject_1.subject_id), response_string
)
self.assertIn(
'"revision_id":"{}"'.format(self.case_subject_1.revision_id),
response_string,
)
self.assertIn('"action_type":"{}"'.format(ACTIVITY_TYPE_EDIT), response_string)
def test_edit_case_subject_fails_with_incomplete_data(self):
url = "/cases/case-subjects/" + str(self.case_subject_1.revision_id) + "/"
url = self.BASE_URL + str(self.case_subject_1.revision_id) + "/"
data = {
"address": self.case_subject_1.address,
......@@ -212,7 +247,7 @@ class CaseSubjectViewTest(APITestCase):
)
def test_soft_delete_case_subject_success(self):
url = "/cases/case-subjects/" + str(self.case_subject_1.revision_id) + "/"
url = self.BASE_URL + str(self.case_subject_1.revision_id) + "/"
prev_deleted_all_count = CaseSubject.objects.all(with_deleted=True).count()
prev_deleted_count = CaseSubject.objects.deleted().count()
......@@ -230,8 +265,23 @@ class CaseSubjectViewTest(APITestCase):
self.assertEqual(current_deleted_all_count, prev_deleted_all_count)
self.assertEqual(current_deleted_count, prev_deleted_count + 1)
# Have case subject deletion log
response = self.client.get("/logs/")
response_string = response.rendered_content.decode("utf-8")
self.assertIn(
'"object_id":"{}"'.format(self.case_subject_1.subject_id), response_string
)
self.assertIn(
'"revision_id":"{}"'.format(self.case_subject_1.revision_id),
response_string,
)
self.assertIn(
'"action_type":"{}"'.format(ACTIVITY_TYPE_DELETE), response_string
)
def test_delete_inactive_case_subject_fails(self):
url = "/cases/case-subjects/" + str(self.case_subject_3.revision_id) + "/"
url = self.BASE_URL + str(self.case_subject_3.revision_id) + "/"
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
......@@ -12,13 +12,21 @@ from apps.cases.tests.factories.cases import (
PositiveCaseFactory,
InvestigationCaseFactory,
)
from apps.constants import HEADER_PREFIX
from apps.constants import TIMEZONE
from apps.constants import (
HEADER_PREFIX,
TIMEZONE,
ACTIVITY_TYPE_CREATE,
ACTIVITY_TYPE_EDIT,
ACTIVITY_TYPE_DELETE,
)
class InvestigationCaseViewTest(APITestCase):
@classmethod
def setUpTestData(self):
self.BASE_URL = "/cases/investigation-cases/"
self.user_1 = UserFactory(username="user_1", password="justpass")
self.author = AccountFactory(user=self.user_1, admin=True)
self.token_1, _ = Token.objects.get_or_create(user=self.user_1)
......@@ -49,7 +57,7 @@ class InvestigationCaseViewTest(APITestCase):
self.client = APIClient(HTTP_AUTHORIZATION=HEADER_PREFIX + self.token_1.key)
def test_list_investigation_cases_success(self):
url = "/cases/investigation-cases/"
url = self.BASE_URL
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
......@@ -69,7 +77,7 @@ class InvestigationCaseViewTest(APITestCase):
self.assertIn(str(self.case.author.id), response_string)
def test_list_investigation_case_paginate_failed(self):
url = "/cases/investigation-cases/?page=100"
url = self.BASE_URL + "?page=100"
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
......@@ -79,7 +87,7 @@ class InvestigationCaseViewTest(APITestCase):
self.assertIn('"detail":"Invalid page."', response_string)
def test_list_investigation_case_filter_success(self):
url = "/cases/investigation-cases/?risk_factors=" + self.case.risk_factors
url = self.BASE_URL + "?risk_factors=" + self.case.risk_factors
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
......@@ -89,7 +97,7 @@ class InvestigationCaseViewTest(APITestCase):