Fakultas Ilmu Komputer UI

Commit 9a170c4e authored by Jonathan Christopher Jakub's avatar Jonathan Christopher Jakub
Browse files

Merge branch 'rework-model-revisions' into 'staging'

Implement rework on model revisions

See merge request !35
parents 20842e86 c10e67ad
Pipeline #41513 passed with stages
in 20 minutes and 29 seconds
from django_filters import FilterSet, CharFilter
ACCOUNT_FILTERSET_FIELDS = (
"name",
"email",
"phone_number",
"area",
"is_admin",
"is_verified",
"is_active",
)
from apps.accounts.models import Account
ACCOUNT_SEARCH_FIELDS = (
"name",
"email",
"phone_number",
"area",
"is_admin",
"is_verified",
"is_active",
)
class AccountFilter(FilterSet):
username = CharFilter(field_name="user__username")
class Meta:
model = Account
fields = [
"username",
"name",
"email",
"phone_number",
"area",
"is_admin",
"is_verified",
]
ACCOUNT_ORDERING_FIELDS = (
"name",
"email",
"phone_number",
"area",
"is_admin",
"is_verified",
"is_active",
"created_at",
)
# Generated by Django 3.0.1 on 2020-04-18 18:56
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
("accounts", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="AccountHistory",
fields=[
(
"revision_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("object_id", models.UUIDField(default=uuid.uuid4, editable=False)),
(
"action_type",
models.CharField(
choices=[
("Create", "Create"),
("Edit", "Edit"),
("Delete", "Delete"),
],
max_length=64,
),
),
("recorded_at", models.DateTimeField(auto_now_add=True)),
("user_id", models.IntegerField()),
("name", models.CharField(max_length=128)),
("email", models.EmailField(max_length=128)),
("phone_number", models.CharField(max_length=64)),
("area", models.CharField(max_length=128)),
("is_admin", models.BooleanField(default=False)),
("is_verified", models.BooleanField(default=False)),
("is_active", models.BooleanField(default=False)),
],
options={
"verbose_name_plural": "account histories",
"db_table": "account_history",
"ordering": ["-recorded_at"],
},
),
migrations.AlterModelOptions(
name="account",
options={"ordering": ["-created_at"], "verbose_name_plural": "accounts"},
),
migrations.AddField(
model_name="account",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
]
# Generated by Django 3.0.1 on 2020-04-19 11:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0002_auto_20200419_0156"),
]
operations = [
migrations.AddField(
model_name="accounthistory",
name="author",
field=models.UUIDField(null=True),
),
]
# Generated by Django 3.0.1 on 2020-04-19 11:29
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("accounts", "0003_accounthistory_author"),
]
operations = [
migrations.AddField(
model_name="account",
name="author",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="account",
to="accounts.Account",
),
),
]
......@@ -2,10 +2,12 @@ import uuid
from django.contrib.auth.models import User
from django.db import models
from apps.commons.managers import SoftDeleteManager
from apps.commons.models import HistoryEnabledModel, HistoryModel
class Account(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.OneToOneField(User, on_delete=models.CASCADE)
class AccountHistory(HistoryModel):
user_id = models.IntegerField(null=False)
name = models.CharField(max_length=128)
email = models.EmailField(max_length=128)
phone_number = models.CharField(max_length=64)
......@@ -13,15 +15,46 @@ class Account(models.Model):
is_admin = models.BooleanField(default=False)
is_verified = models.BooleanField(default=False)
is_active = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
author = models.UUIDField(null=True)
objects = models.Manager()
class Meta:
db_table = "account_history"
verbose_name_plural = "account histories"
ordering = ["-recorded_at"]
def __str__(self):
return f"[History] Rev. {self.revision_id} - {self.name}"
class Account(HistoryEnabledModel):
user = models.OneToOneField(User, on_delete=models.CASCADE)
name = models.CharField(max_length=128)
email = models.EmailField(max_length=128)
phone_number = models.CharField(max_length=64)
area = models.CharField(max_length=128)
is_admin = models.BooleanField(default=False)
is_verified = models.BooleanField(default=False)
is_active = models.BooleanField(default=False)
author = models.ForeignKey(
"self",
blank=True,
null=True,
on_delete=models.DO_NOTHING,
related_name="account",
)
history_class = AccountHistory
relation_fields = ["user"]
objects = SoftDeleteManager()
class Meta:
db_table = "account"
verbose_name_plural = "accounts"
ordering = ["-created_at"]
def __str__(self):
if self.is_admin:
return f"[Admin] {self.user.username}"
return f"[Officer] {self.user.username}"
account_role = "[Admin]" if self.is_admin else "[Officer]"
return f"{account_role} {self.user.username}"
......@@ -5,7 +5,7 @@ from apps.accounts.models import Account
class AccountSerializer(serializers.ModelSerializer):
username = serializers.CharField(source="user.username", required=False)
username = serializers.CharField(source="user.username", read_only=True)
class Meta:
model = Account
......@@ -26,6 +26,10 @@ class AccountSerializer(serializers.ModelSerializer):
"username",
]
def save(self):
account = self.context.get('request').user.account
super(AccountSerializer, self).save(author=account)
class AccountRegisterSerializer(serializers.ModelSerializer):
username = serializers.CharField(max_length=128)
......@@ -47,3 +51,7 @@ class AccountRegisterSerializer(serializers.ModelSerializer):
def validate_password(self, value):
password_validation.validate_password(value)
return value
def to_representation(self, instance):
serializer = AccountSerializer(instance)
return serializer.data
......@@ -6,7 +6,7 @@ from rest_framework.authtoken.models import Token
from rest_framework.test import APITestCase, APIClient
from apps.accounts.tests.factories.accounts import AccountFactory, UserFactory
from apps.accounts.models import Account
from apps.accounts.models import Account, AccountHistory
from apps.constants import (
HEADER_PREFIX,
ACTIVITY_TYPE_CREATE,
......@@ -17,20 +17,21 @@ from apps.constants import (
class AccountViewTest(APITestCase):
@classmethod
def setUpTestData(self):
self.BASE_URL = "/accounts/"
self.PROFILE_URL = self.BASE_URL + "me/"
def setUpTestData(cls):
cls.BASE_URL = "/accounts/"
cls.PROFILE_URL = cls.BASE_URL + "me/"
cls.LOGS_URL = "/logs/"
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)
self.officer = AccountFactory(admin=False, user=self.user_2)
self.accounts = [self.admin, self.officer]
cls.user_1 = UserFactory(username="user_1", password="justpass")
cls.user_2 = UserFactory(username="user_2", password="justpass")
cls.admin = AccountFactory(admin=True, user=cls.user_1)
cls.officer = AccountFactory(admin=False, user=cls.user_2)
cls.accounts = [cls.admin, cls.officer]
self.token_1, _ = Token.objects.get_or_create(user=self.user_1)
self.token_2, _ = Token.objects.get_or_create(user=self.user_2)
cls.token_1, _ = Token.objects.get_or_create(user=cls.user_1)
cls.token_2, _ = Token.objects.get_or_create(user=cls.user_2)
self.faker = Faker()
cls.faker = Faker()
def setUp(self):
self.client = APIClient(HTTP_AUTHORIZATION=HEADER_PREFIX + self.token_1.key)
......@@ -42,6 +43,12 @@ class AccountViewTest(APITestCase):
officer_str = f"[Officer] {self.officer.user.username}"
self.assertEqual(officer_str, str(self.officer))
def test_history_string_representation(self):
history = AccountHistory.objects.all().first()
history_str = f"[History] Rev. {history.revision_id} - {history.name}"
self.assertEqual(history_str, str(history))
def test_list_all_accounts_success(self):
url = self.BASE_URL
......@@ -51,33 +58,6 @@ class AccountViewTest(APITestCase):
self.assertIn('"user_1"', response_string)
self.assertIn('"user_2"', response_string)
def test_list_all_accounts_paginate_failed(self):
url = self.BASE_URL + "?page=100"
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
response_string = response.rendered_content.decode("utf-8")
self.assertIn('"detail":"Invalid page."', response_string)
def test_list_all_accounts_filter_success(self):
url = self.BASE_URL + "?username=" + self.user_1.username
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_string = response.rendered_content.decode("utf-8")
self.assertIn('"count":1', response_string)
def test_list_all_accounts_filter_failed(self):
url = self.BASE_URL + "?username=1234567890"
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_string = response.rendered_content.decode("utf-8")
self.assertIn('"count":0', response_string)
def test_retrieve_account_success(self):
url = self.BASE_URL + str(self.officer.id) + "/"
response = self.client.get(url)
......@@ -142,32 +122,14 @@ class AccountViewTest(APITestCase):
# 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 = self.client.get(self.LOGS_URL)
response_string = response.rendered_content.decode("utf-8")
self.assertIn('"object_id":"{}"'.format(new_officer_id), response_string)
self.assertIn(f'"object_id":"{new_officer_id}"', response_string)
self.assertIn(
'"action_type":"{}"'.format(ACTIVITY_TYPE_CREATE), response_string
f'"action_type":"{ACTIVITY_TYPE_CREATE}"', response_string
)
def test_username_lowercased(self):
url = self.BASE_URL
data = {
"name": self.faker.name(),
"username": "aBcDeFgH",
"password": "justpass",
"email": self.faker.email(),
"phone_number": self.faker.phone_number(),
"area": self.faker.city(),
"is_admin": False,
}
self.client.post(
path=url, data=data, format="json",
)
self.assertTrue(Account.objects.filter(user__username="abcdefgh").exists())
def test_create_new_account_fails_with_poor_password(self):
url = self.BASE_URL
_account_id = self.faker.email()
......@@ -210,11 +172,11 @@ class AccountViewTest(APITestCase):
)
# Have account update log
response = self.client.get("/logs/")
response = self.client.get(self.LOGS_URL)
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)
self.assertIn(f'"object_id":"{self.officer.id}"', response_string)
self.assertIn(f'"action_type":"{ACTIVITY_TYPE_EDIT}"', response_string)
def test_edit_account_fail_without_complete_fields(self):
url = self.BASE_URL + str(self.officer.id) + "/"
......@@ -231,19 +193,32 @@ class AccountViewTest(APITestCase):
def test_delete_success(self):
url = self.BASE_URL + str(self.officer.id) + "/"
account_prev_count = Account.objects.all().count()
account_history_prev_count = AccountHistory.objects.all().count()
account_deleted_prev_count = Account.objects.deleted().count()
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
# Have account deletion log
response = self.client.get("/logs/")
response = self.client.get(self.LOGS_URL)
response_string = response.rendered_content.decode("utf-8")
self.assertIn('"object_id":"{}"'.format(self.officer.id), response_string)
self.assertIn(f'"object_id":"{self.officer.id}"', response_string)
self.assertIn(
'"action_type":"{}"'.format(ACTIVITY_TYPE_DELETE), response_string
f'"action_type":"{ACTIVITY_TYPE_DELETE}"', response_string
)
account_current_count = Account.objects.all().count()
account_history_current_count = AccountHistory.objects.all().count()
account_deleted_current_count = Account.objects.deleted().count()
self.assertEqual(account_current_count, account_prev_count - 1)
self.assertEqual(account_history_current_count, account_history_prev_count + 1)
self.assertEqual(account_deleted_current_count, account_deleted_prev_count + 1)
def test_retrieve_current_profile_success(self):
url = self.PROFILE_URL
......
......@@ -3,17 +3,22 @@ from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.filters import SearchFilter, OrderingFilter
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
from apps.accounts.filters import AccountFilter
from apps.accounts.filters import (
ACCOUNT_FILTERSET_FIELDS,
ACCOUNT_ORDERING_FIELDS,
ACCOUNT_SEARCH_FIELDS,
)
from apps.accounts.models import Account
from apps.accounts.serializers import (
AccountSerializer,
AccountRegisterSerializer,
)
from apps.commons.permissions import (
IsSelfOrAdministrator,
IsAuthenticated,
CreateOnly,
)
from apps.constants import (
......@@ -22,85 +27,39 @@ from apps.constants import (
ACTIVITY_TYPE_EDIT,
ACTIVITY_TYPE_DELETE,
)
from apps.logs.models import Log
class AccountViewSet(viewsets.ViewSet):
queryset = Account.objects.all().select_related("user").order_by("-created_at")
filter_backends = (DjangoFilterBackend,)
permission_classes = [
IsSelfOrAdministrator | CreateOnly,
]
def list(self, request):
paginator = PageNumberPagination()
filtered_set = AccountFilter(request.GET, queryset=self.queryset).qs
context = paginator.paginate_queryset(filtered_set, request)
serializer = AccountSerializer(context, many=True)
return paginator.get_paginated_response(serializer.data)
def retrieve(self, request, pk=None):
instance = get_object_or_404(self.queryset, pk=pk)
self.check_object_permissions(request, instance)
serializer = AccountSerializer(instance)
return Response(serializer.data, status=status.HTTP_200_OK)
def create(self, request):
serializer = AccountRegisterSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
class AccountViewSet(viewsets.ModelViewSet):
serializer_class = AccountSerializer
queryset = Account.objects.all()
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
filterset_fields = ACCOUNT_FILTERSET_FIELDS
permission_classes = (IsAuthenticated | CreateOnly, )
pagination_class = PageNumberPagination
search_fields = ACCOUNT_SEARCH_FIELDS
ordering_fields = ACCOUNT_ORDERING_FIELDS
def get_queryset(self):
user = self.request.user
account = Account.objects.filter(user=user)
if account.first().is_admin:
return Account.objects.all()
return account
def get_serializer_class(self):
if self.action in ['create']:
return AccountRegisterSerializer
return AccountSerializer
def perform_create(self, serializer):
username = serializer.validated_data.pop("username").lower()
password = serializer.validated_data.pop("password")
user = User.objects.create_user(username=username, password=password)
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=account,
)
Account.objects.create(user=user, **serializer.validated_data)
return Response(
AccountSerializer(account).data, status=status.HTTP_201_CREATED,
)
def update(self, request, pk=None):
instance = get_object_or_404(self.queryset, pk=pk)
self.check_object_permissions(request, instance)
serializer = AccountSerializer(instance, data=request.data)
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)
def perform_destroy(self, instance):
instance.delete(author=self.request.user.account)
@action(detail=False, methods=["get"], url_path="me")
def profile(self, request):
......
from django_filters import FilterSet
from apps.cases.models import (
CaseSubject,
InvestigationCase,
MonitoringCase,
)
class CaseSubjectFilter(FilterSet):
class Meta:
model = CaseSubject
fields = [
"age",
"is_male",
"address",
"district",
"sub_district",