Skip to content

Commit c3167d9

Browse files
committed
Implement primitive model and API handlers for Category
This adds new feature that gets together models that has same attribute (feature, purpose and so on).
1 parent 72294e5 commit c3167d9

19 files changed

+320
-0
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
* Enable to get items considering with Alias name (by simple solution)
77
Contributed by @userlocalhost
88

9+
* Added new feature that gets together models that has same attribute
10+
(feature, purpose and so on).
11+
Contributed by @userlocalhost
12+
913
### Changed
1014

1115
### Fixed

airone/lib/test.py

+13
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
import inspect
33
import os
44
import sys
5+
from typing import List
56

67
from django.conf import settings
78
from django.test import Client, TestCase, override_settings
89
from pytz import timezone
910

1011
from airone.lib.acl import ACLType
1112
from airone.lib.types import AttrType
13+
from category.models import Category
1214
from entity.models import Entity, EntityAttr
1315
from entry.models import Entry
1416
from user.models import User
@@ -148,6 +150,17 @@ def add_entry(self, user: User, name: str, schema: Entity, values={}, is_public=
148150

149151
return entry
150152

153+
def create_category(self, user: User, name: str, note: str = "", models: List[Entity] = []):
154+
# create target Category instance
155+
category = Category.objects.create(name=name, note=note, created_user=user)
156+
157+
# attach created category to each specified Models
158+
for model in models:
159+
if model.is_active:
160+
model.categories.add(category)
161+
162+
return category
163+
151164
def _do_login(self, uname, is_superuser=False) -> User:
152165
# create test user to authenticate
153166
user = User(username=uname, is_superuser=is_superuser)

airone/settings_common.py

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class Common(Configuration):
6464
"simple_history",
6565
"storages",
6666
"trigger",
67+
"category",
6768
]
6869

6970
if os.path.exists(BASE_DIR + "/custom_view"):

airone/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
re_path(r"^webhook/", include(("webhook.urls", "webhook"))),
4343
re_path(r"^role/", include(("role.urls", "role"))),
4444
re_path(r"^trigger/", include(("trigger.urls", "trigger"))),
45+
re_path(r"^category/", include(("category.urls", "category"))),
4546
]
4647

4748
if settings.DEBUG:

category/__init__.py

Whitespace-only changes.

category/admin.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Register your models here.

category/api_v2/__init__.py

Whitespace-only changes.

category/api_v2/serializers.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from rest_framework import serializers
2+
3+
from category.models import Category
4+
from entity.api_v2.serializers import EntitySerializer
5+
from entity.models import Entity
6+
7+
8+
class CategoryListSerializer(serializers.ModelSerializer):
9+
models = EntitySerializer(many=True)
10+
11+
class Meta:
12+
model = Category
13+
fields = ["id", "name", "note", "models"]
14+
15+
16+
class CategoryCreateSerializer(serializers.ModelSerializer):
17+
id = serializers.IntegerField(required=False, read_only=True)
18+
models = serializers.ListField(write_only=True, required=False, default=[])
19+
20+
class Meta:
21+
model = Category
22+
fields = ["id", "name", "note", "models"]
23+
24+
def create(self, validated_data):
25+
# craete Category instance
26+
category = Category.objects.create(
27+
created_user=self.context["request"].user,
28+
**{k: v for (k, v) in validated_data.items() if k != "models"},
29+
)
30+
31+
# make relations created Category with specified Models
32+
for model in Entity.objects.filter(id__in=validated_data.get("models", []), is_active=True):
33+
model.categories.add(category)
34+
35+
return category
36+
37+
38+
class CategoryUpdateSerializer(serializers.ModelSerializer):
39+
models = serializers.ListField(write_only=True, required=False, default=[])
40+
41+
class Meta:
42+
model = Category
43+
fields = ["name", "note", "models"]
44+
45+
def update(self, category: Category, validated_data):
46+
curr_model_ids = [x.id for x in category.models.filter(is_active=True)]
47+
48+
# remove models that are unselected from current ones
49+
removing_model_ids = list(
50+
set(curr_model_ids) - set(validated_data.get("models", curr_model_ids))
51+
)
52+
for model in Entity.objects.filter(id__in=removing_model_ids, is_active=True):
53+
model.categories.remove(category)
54+
55+
# add new models that are added to current ones
56+
adding_model_ids = list(
57+
set(validated_data.get("models", curr_model_ids)) - set(curr_model_ids)
58+
)
59+
for model in Entity.objects.filter(id__in=adding_model_ids, is_active=True):
60+
model.categories.add(category)
61+
62+
return category

category/api_v2/urls.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from django.urls import path
2+
3+
from . import views
4+
5+
urlpatterns = [
6+
path(
7+
"",
8+
views.CategoryAPI.as_view(
9+
{
10+
"get": "list",
11+
"post": "create",
12+
}
13+
),
14+
),
15+
path(
16+
"<int:pk>/",
17+
views.CategoryAPI.as_view(
18+
{
19+
"get": "retrieve",
20+
"put": "update",
21+
"delete": "destroy",
22+
}
23+
),
24+
),
25+
]

category/api_v2/views.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from django_filters.rest_framework import DjangoFilterBackend
2+
from rest_framework import filters, status, viewsets
3+
from rest_framework.pagination import LimitOffsetPagination
4+
from rest_framework.permissions import IsAuthenticated
5+
from rest_framework.request import Request
6+
from rest_framework.response import Response
7+
8+
from airone.lib.drf import ObjectNotExistsError
9+
from category.api_v2.serializers import (
10+
CategoryCreateSerializer,
11+
CategoryListSerializer,
12+
CategoryUpdateSerializer,
13+
)
14+
from category.models import Category
15+
from entity.api_v2.views import EntityPermission
16+
17+
18+
class CategoryAPI(viewsets.ModelViewSet):
19+
pagination_class = LimitOffsetPagination
20+
permission_classes = [IsAuthenticated & EntityPermission]
21+
filter_backends = [DjangoFilterBackend, filters.OrderingFilter, filters.SearchFilter]
22+
search_fields = ["name"]
23+
ordering = ["name"]
24+
25+
def get_serializer_class(self):
26+
serializer = {
27+
"create": CategoryCreateSerializer,
28+
"update": CategoryUpdateSerializer,
29+
}
30+
return serializer.get(self.action, CategoryListSerializer)
31+
32+
def get_queryset(self):
33+
return Category.objects.filter(is_active=True)
34+
35+
def destroy(self, request: Request, *args, **kwargs) -> Response:
36+
category: Category = self.get_object()
37+
if not category.is_active:
38+
raise ObjectNotExistsError("specified entry has already been deleted")
39+
40+
# delete specified category
41+
category.delete()
42+
43+
return Response(status=status.HTTP_204_NO_CONTENT)

category/apps.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class CategoryConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "category"

category/migrations/__init__.py

Whitespace-only changes.

category/models.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from django.db import models
2+
3+
from acl.models import ACLBase
4+
5+
6+
class Category(ACLBase):
7+
note = models.CharField(max_length=500, blank=True, default="")

category/tests/__init__.py

Whitespace-only changes.

category/tests/test_api_v2.py

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import json
2+
from typing import List
3+
4+
from airone.lib.test import AironeViewTest
5+
from category.models import Category
6+
from entity.models import Entity
7+
from user.models import User
8+
9+
10+
class ViewTest(AironeViewTest):
11+
def setUp(self):
12+
super(ViewTest, self).setUp()
13+
14+
self.user: User = self.guest_login()
15+
16+
def test_list(self):
17+
# initialize Model and Categories for testing API processing
18+
model: Entity = self.create_entity(self.user, "Model")
19+
categories: List[Category] = [
20+
self.create_category(self.user, "Category-%d" % n, "Note-%d" % n, [model])
21+
for n in range(3)
22+
]
23+
24+
# get all Categories
25+
resp = self.client.get("/category/api/v2/")
26+
self.assertEqual(resp.status_code, 200)
27+
28+
# check returned data has expected values
29+
self.assertEqual(resp.json()["count"], 3)
30+
self.assertEqual(
31+
resp.json()["results"],
32+
[
33+
{
34+
"id": x.id,
35+
"name": x.name,
36+
"note": x.note,
37+
"models": [{"id": model.id, "name": model.name, "is_public": True}],
38+
}
39+
for x in categories
40+
],
41+
)
42+
43+
def test_list_include_deleted_category(self):
44+
pass
45+
46+
def test_get(self):
47+
# initialize Models and Categories for testing API processing
48+
models: List[Entity] = [self.create_entity(self.user, "Model-%d" % n) for n in range(3)]
49+
category: Category = self.create_category(self.user, "Category", "note", models)
50+
51+
# get specified Category
52+
resp = self.client.get("/category/api/v2/%d/" % category.id)
53+
self.assertEqual(resp.status_code, 200)
54+
self.assertEqual(
55+
resp.json(),
56+
{
57+
"id": category.id,
58+
"name": category.name,
59+
"note": category.note,
60+
"models": [{"id": x.id, "name": x.name, "is_public": True} for x in models],
61+
},
62+
)
63+
64+
def test_get_include_deleted_model(self):
65+
pass
66+
67+
def test_create(self):
68+
# initialize Models for testing API processing
69+
models: List[Entity] = [self.create_entity(self.user, "Model-%d" % n) for n in range(3)]
70+
71+
# send request to create a Category
72+
params = {
73+
"name": "New Category",
74+
"note": "Hoge",
75+
"models": [x.id for x in models],
76+
}
77+
resp = self.client.post("/category/api/v2/", json.dumps(params), "application/json")
78+
self.assertEqual(resp.status_code, 201)
79+
80+
# check Category item is created and has expected attribute values
81+
category = Category.objects.last()
82+
self.assertEqual(category.id, resp.json()["id"])
83+
self.assertEqual(category.name, resp.json()["name"])
84+
self.assertEqual(category.note, resp.json()["note"])
85+
self.assertEqual(category.models.count(), 3)
86+
self.assertEqual(list(category.models.all()), models)
87+
88+
def test_delete(self):
89+
# create Category instance that will be deleted in this test
90+
category: Category = self.create_category(self.user, "Category", "note")
91+
92+
# send request to delete target Category
93+
resp = self.client.delete("/category/api/v2/%d/" % category.id)
94+
self.assertEqual(resp.status_code, 204)
95+
96+
# check target category instance is existed but inactivated
97+
self.assertTrue(Category.objects.filter(id=category.id).exists())
98+
self.assertFalse(Category.objects.filter(id=category.id, is_active=True).exists())
99+
100+
def test_update(self):
101+
"""
102+
Initially, there are 3 models (M0, M1, M2) and Category has M0 and M1
103+
Then, this test sends a request to change its beloning category to M1 and M2.
104+
"""
105+
models: List[Entity] = [self.create_entity(self.user, "M%d" % n) for n in range(3)]
106+
category: Category = self.create_category(
107+
self.user, "Category", "note", [models[0], models[1]]
108+
)
109+
self.assertEqual(list(category.models.all()), [models[0], models[1]])
110+
111+
params = {
112+
"name": "Updated Category",
113+
"note": "Updated Note",
114+
"models": [models[1].id, models[2].id],
115+
}
116+
resp = self.client.put(
117+
"/category/api/v2/%s/" % category.id, json.dumps(params), "application/json"
118+
)
119+
self.assertEqual(resp.status_code, 200)
120+
121+
# check all data is updated expectedly
122+
self.assertEqual(list(category.models.all()), [models[1], models[2]])

category/urls.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.urls import include, re_path
2+
3+
urlpatterns = [
4+
re_path(r"^api/v2/", include(("category.api_v2.urls", "category.api_v2"))),
5+
]

category/views.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Create your views here.

entity/models.py

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from acl.models import ACLBase
88
from airone.lib.acl import ACLObjType
9+
from category.models import Category
910
from webhook.models import Webhook
1011

1112

@@ -100,6 +101,9 @@ class Entity(ACLBase):
100101

101102
history = HistoricalRecords(excluded_fields=["status", "updated_time"])
102103

104+
# The Category that groups Models according to their purpose (which is defined by User)
105+
categories = models.ManyToManyField(Category, default=[], related_name="models")
106+
103107
def __init__(self, *args, **kwargs):
104108
super(Entity, self).__init__(*args, **kwargs)
105109
self.objtype = ACLObjType.Entity

entity/tests/test_model.py

+25
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.test import TestCase
55

66
from airone.lib.types import AttrType
7+
from category.models import Category
78
from entity.admin import EntityAttrResource, EntityResource
89
from entity.models import Entity, EntityAttr
910
from user.models import User
@@ -401,3 +402,27 @@ def test_max_attributes_per_entity(self):
401402
created_user=self._test_user,
402403
parent_entity=entity,
403404
)
405+
406+
def test_category(self):
407+
models = [
408+
Entity.objects.create(name="Model-%d" % n, created_user=self._test_user)
409+
for n in range(3)
410+
]
411+
categories = [
412+
Category.objects.create(name="Category-%d" % n, created_user=self._test_user)
413+
for n in range(3)
414+
]
415+
416+
# set relation from single Model to multiple Category
417+
for n in range(3):
418+
models[0].categories.add(categories[n])
419+
420+
# check each categories are set properly
421+
self.assertEqual(list(models[0].categories.all()), categories)
422+
423+
# set relation from single Category to multiple Model
424+
for n in range(3):
425+
models[n].categories.add(categories[0])
426+
427+
# check each models are set properly
428+
self.assertEqual(list(categories[0].models.all()), models)

0 commit comments

Comments
 (0)