Skip to content

Commit

Permalink
Merge pull request #1360 from userlocalhost/feature/new_application/c…
Browse files Browse the repository at this point in the history
…ategory

Implement primitive model and API handlers for Category
  • Loading branch information
hinashi authored Feb 5, 2025
2 parents 02c1a2a + 6222eb5 commit 971e0bd
Show file tree
Hide file tree
Showing 40 changed files with 1,133 additions and 36 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
* Enable to get items considering with Alias name (by simple solution)
Contributed by @userlocalhost

* Added new feature that gets together models that has same attribute
(feature, purpose and so on).
Contributed by @userlocalhost

### Changed
* Optimize role import for job execution
Contributed by @tsunoda-takahiro
Expand Down
24 changes: 14 additions & 10 deletions acl/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from acl.models import ACLBase
from airone.lib.acl import ACLObjType, ACLType
from airone.lib.drf import IncorrectTypeError, ObjectNotExistsError
from category.models import Category
from entity.models import Entity, EntityAttr
from entry.models import Attribute, Entry
from role.models import HistoricalPermission, Role
Expand Down Expand Up @@ -184,16 +185,19 @@ def update(self, instance: ACLBase, validated_data):

@staticmethod
def _get_acl_model(object_id):
if int(object_id) == ACLObjType.Entity:
return Entity
if int(object_id) == ACLObjType.Entry:
return Entry
elif int(object_id) == ACLObjType.EntityAttr:
return EntityAttr
elif int(object_id) == ACLObjType.EntryAttr:
return Attribute
else:
return ACLBase
match int(object_id):
case ACLObjType.Entity:
return Entity
case ACLObjType.Entry:
return Entry
case ACLObjType.EntityAttr:
return EntityAttr
case ACLObjType.EntryAttr:
return Attribute
case ACLObjType.Category:
return Category
case _:
return ACLBase

@staticmethod
def _set_permission(
Expand Down
2 changes: 2 additions & 0 deletions acl/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ def get_subclass_object(self):
model = importlib.import_module("entry.models").Entry
case ACLObjType.EntryAttr:
model = importlib.import_module("entry.models").Attribute
case ACLObjType.Category:
model = importlib.import_module("category.models").Category
case _:
model = type(self)

Expand Down
1 change: 1 addition & 0 deletions airone/lib/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class ACLObjType(enum.IntEnum):
EntityAttr = 1 << 1
Entry = 1 << 2
EntryAttr = 1 << 3
Category = 1 << 4


class ACLType(enum.IntEnum):
Expand Down
1 change: 1 addition & 0 deletions airone/lib/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def render(request, template, context={}):
"entry": ACLObjType.Entry,
"attrbase": ACLObjType.EntityAttr,
"attr": ACLObjType.EntryAttr,
"category": ACLObjType.Category,
},
}

Expand Down
13 changes: 13 additions & 0 deletions airone/lib/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import inspect
import os
import sys
from typing import List

from django.conf import settings
from django.test import Client, TestCase, override_settings
from pytz import timezone

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

return entry

def create_category(self, user: User, name: str, note: str = "", models: List[Entity] = []):
# create target Category instance
category = Category.objects.create(name=name, note=note, created_user=user)

# attach created category to each specified Models
for model in models:
if model.is_active:
model.categories.add(category)

return category

def _do_login(self, uname, is_superuser=False) -> User:
# create test user to authenticate
user = User(username=uname, is_superuser=is_superuser)
Expand Down
1 change: 1 addition & 0 deletions airone/settings_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class Common(Configuration):
"simple_history",
"storages",
"trigger",
"category",
]

if os.path.exists(BASE_DIR + "/custom_view"):
Expand Down
1 change: 1 addition & 0 deletions airone/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
re_path(r"^webhook/", include(("webhook.urls", "webhook"))),
re_path(r"^role/", include(("role.urls", "role"))),
re_path(r"^trigger/", include(("trigger.urls", "trigger"))),
re_path(r"^category/", include(("category.urls", "category"))),
]

if settings.DEBUG:
Expand Down
2 changes: 1 addition & 1 deletion apiclient/typescript-fetch/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dmm-com/airone-apiclient-typescript-fetch",
"version": "0.3.0",
"version": "0.4.0",
"description": "AirOne APIv2 client in TypeScript",
"main": "src/autogenerated/index.ts",
"scripts": {
Expand Down
Empty file added category/__init__.py
Empty file.
1 change: 1 addition & 0 deletions category/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Register your models here.
Empty file added category/api_v2/__init__.py
Empty file.
86 changes: 86 additions & 0 deletions category/api_v2/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from rest_framework import serializers

from category.models import Category
from entity.models import Entity


# We bordered the situation that ID parameter would be readonly via OpenAPI generate
# when EntitySerializer, which is defined in the entity.api_v2.serializers, was specified.
class EntitySerializer(serializers.ModelSerializer):
id = serializers.IntegerField()

class Meta:
model = Entity
fields = ["id", "name", "is_public"]


class CategoryListSerializer(serializers.ModelSerializer):
models = EntitySerializer(many=True)

class Meta:
model = Category
fields = ["id", "name", "note", "models"]


class CategoryCreateSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False, read_only=True)
models = EntitySerializer(many=True)

class Meta:
model = Category
fields = ["id", "name", "note", "models"]

def create(self, validated_data):
# craete Category instance
category = Category.objects.create(
created_user=self.context["request"].user,
**{k: v for (k, v) in validated_data.items() if k != "models"},
)

# make relations created Category with specified Models
for model in Entity.objects.filter(
id__in=[x["id"] for x in validated_data.get("models", [])], is_active=True
):
model.categories.add(category)

return category


class CategoryUpdateSerializer(serializers.ModelSerializer):
models = EntitySerializer(many=True)

class Meta:
model = Category
fields = ["name", "note", "models"]

def update(self, category: Category, validated_data):
# name and note are updated
updated_fields: list[str] = []
category_name = validated_data.get("name", category.name)
if category_name != category.name:
category.name = category_name
updated_fields.append("name")
category_note = validated_data.get("note", category.note)
if category_note != category.note:
category.note = category_note
updated_fields.append("note")

if len(updated_fields) > 0:
category.save(update_fields=updated_fields)
else:
category.save_without_historical_record()

curr_model_ids = [x.id for x in category.models.filter(is_active=True)]
new_model_ids = [x["id"] for x in validated_data.get("models", [])]

# remove models that are unselected from current ones
removing_model_ids = list(set(curr_model_ids) - set(new_model_ids))
for model in Entity.objects.filter(id__in=removing_model_ids, is_active=True):
model.categories.remove(category)

# add new models that are added to current ones
adding_model_ids = list(set(new_model_ids) - set(curr_model_ids))
for model in Entity.objects.filter(id__in=adding_model_ids, is_active=True):
model.categories.add(category)

return category
25 changes: 25 additions & 0 deletions category/api_v2/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.urls import path

from . import views

urlpatterns = [
path(
"",
views.CategoryAPI.as_view(
{
"get": "list",
"post": "create",
}
),
),
path(
"<int:pk>/",
views.CategoryAPI.as_view(
{
"get": "retrieve",
"put": "update",
"delete": "destroy",
}
),
),
]
50 changes: 50 additions & 0 deletions category/api_v2/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, status, viewsets
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response

from airone.lib.acl import ACLType
from airone.lib.drf import ObjectNotExistsError
from category.api_v2.serializers import (
CategoryCreateSerializer,
CategoryListSerializer,
CategoryUpdateSerializer,
)
from category.models import Category
from entity.api_v2.views import EntityPermission


class CategoryAPI(viewsets.ModelViewSet):
pagination_class = LimitOffsetPagination
permission_classes = [IsAuthenticated & EntityPermission]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter, filters.SearchFilter]
search_fields = ["name"]
ordering = ["name"]

def get_serializer_class(self):
serializer = {
"create": CategoryCreateSerializer,
"update": CategoryUpdateSerializer,
}
return serializer.get(self.action, CategoryListSerializer)

def get_queryset(self):
# get items that has permission to read
targets = []
for category in Category.objects.filter(is_active=True):
if self.request.user.has_permission(category, ACLType.Readable):
targets.append(category.id)

return Category.objects.filter(id__in=targets)

def destroy(self, request: Request, *args, **kwargs) -> Response:
category: Category = self.get_object()
if not category.is_active:
raise ObjectNotExistsError("specified entry has already been deleted")

# delete specified category
category.delete()

return Response(status=status.HTTP_204_NO_CONTENT)
6 changes: 6 additions & 0 deletions category/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class CategoryConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "category"
Empty file added category/migrations/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions category/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.db import models

from acl.models import ACLBase, ACLObjType


class Category(ACLBase):
note = models.CharField(max_length=500, blank=True, default="")

def __init__(self, *args, **kwargs):
super(Category, self).__init__(*args, **kwargs)
self.objtype = ACLObjType.Category
Empty file added category/tests/__init__.py
Empty file.
Loading

0 comments on commit 971e0bd

Please sign in to comment.