Skip to content

Commit

Permalink
Merge pull request #1343 from userlocalhost/feature/entry/introduce_a…
Browse files Browse the repository at this point in the history
…lias

Introduced new model of AliasEntry
  • Loading branch information
hinashi authored Dec 25, 2024
2 parents 6c425a9 + c0b4203 commit 28d8130
Show file tree
Hide file tree
Showing 35 changed files with 1,021 additions and 56 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## In development

### Added
* Introduced a new feature to identify Item using another names that are related with
specific Item (it is named as Alias).
Contributed by @hinashi, @userlocalhost

* Enable to pass general external parameters(`extendedGeneralParameters`) to React-UI
implementation via Django template.
Contributed by @userlocalhost, @hinashi
Expand Down
14 changes: 7 additions & 7 deletions airone/lib/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,6 @@ def add_entry(self, user: User, name: str, schema: Entity, values={}, is_public=

return entry


class AironeViewTest(AironeTestCase):
def setUp(self):
super(AironeViewTest, self).setUp()

self.client = Client()

def _do_login(self, uname, is_superuser=False) -> User:
# create test user to authenticate
user = User(username=uname, is_superuser=is_superuser)
Expand All @@ -171,6 +164,13 @@ def admin_login(self) -> User:
def guest_login(self, uname="guest") -> User:
return self._do_login(uname)


class AironeViewTest(AironeTestCase):
def setUp(self):
super(AironeViewTest, self).setUp()

self.client = Client()

def open_fixture_file(self, fname):
test_file_path = inspect.getfile(self.__class__)
test_base_path = os.path.dirname(test_file_path)
Expand Down
18 changes: 18 additions & 0 deletions api_v1/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,23 @@ def side_effect():
# checking that CREATING flag is unset after finishing this processing
self.assertFalse(entry.get_status(Entry.STATUS_CREATING))

def test_create_entry_when_duplicated_alias_exists(self):
user = self.guest_login()

model = self.create_entity(user, "Mountain")
item = self.add_entry(user, "Everest", model)
item.add_alias("Chomolungma")

# send request to try to create Item that has duplicated name with another Alias
params = {
"entity": model.name,
"name": "Chomolungma",
"attrs": {},
}
resp = self.client.post("/api/v1/entry", json.dumps(params), "application/json")
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.json(), {"result": "Duplicate named Alias is existed"})

def test_post_entry_with_invalid_params(self):
admin = self.admin_login()

Expand Down Expand Up @@ -395,6 +412,7 @@ def test_post_entry_with_invalid_params(self):
# is created at the last request to create 'valid-entry'.
params = {"name": "valid-entry", "entity": entity.name, "attrs": {"ref": "r-1"}}
resp = self.client.post("/api/v1/entry", json.dumps(params), "application/json")
print("[onix-test] resp: %s" % str(resp.content.decode("utf-8")))
self.assertEqual(resp.status_code, 200)

entry = Entry.objects.get(schema=entity, name="valid-entry")
Expand Down
21 changes: 21 additions & 0 deletions api_v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,30 @@ def _update_entry_name(entry):
status=status.HTTP_400_BAD_REQUEST,
)

# Get target Item from ID
entry = Entry.objects.get(id=sel.validated_data["id"])

# Check user has permission to update this Item
if not request.user.has_permission(entry, ACLType.Writable):
return Response(
{"result": "Permission denied to update entry"},
status=status.HTTP_400_BAD_REQUEST,
)

# Abort updating processing when duplicated named Alias exists
if not sel.validated_data["entity"].is_available(
sel.validated_data["name"], [entry.id]
):
return Response(
{"result": "Duplicate named Alias is existed"},
status=status.HTTP_400_BAD_REQUEST,
)

will_notify_update_entry = _update_entry_name(entry)

elif Entry.objects.filter(**entry_condition).exists():
entry = Entry.objects.get(**entry_condition)

if not request.user.has_permission(entry, ACLType.Writable):
return Response(
{"result": "Permission denied to update entry"},
Expand All @@ -95,6 +109,13 @@ def _update_entry_name(entry):
will_notify_update_entry = _update_entry_name(entry)

else:
# Abort creating Item when duplicated named Alias exists
if not sel.validated_data["entity"].is_available(entry_condition["name"]):
return Response(
{"result": "Duplicate named Alias is existed"},
status=status.HTTP_400_BAD_REQUEST,
)

# This is the processing just in case for safety not to create duplicate Entries
# when multiple requests passed through existance check. Even through multiple
# requests coming here, Django prevents from creating multiple Entries.
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.1.0",
"version": "0.2.1",
"description": "AirOne APIv2 client in TypeScript",
"main": "src/autogenerated/index.ts",
"scripts": {
Expand Down
Binary file added dmm-com-pagoda-core-1.0.2.tgz
Binary file not shown.
15 changes: 15 additions & 0 deletions entity/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List

from django.conf import settings
from django.db import models
from simple_history.models import HistoricalRecords
Expand Down Expand Up @@ -107,3 +109,16 @@ def save(self, *args, **kwargs) -> None:
if max_entities and Entity.objects.count() >= max_entities:
raise RuntimeError("The number of entities is over the limit")
return super(Entity, self).save(*args, **kwargs)

def is_available(self, name: str, exclude_item_ids: List[int] = []) -> bool:
from entry.models import AliasEntry, Entry

if (
Entry.objects.filter(name=name, schema=self, is_active=True)
.exclude(id__in=exclude_item_ids)
.exists()
):
return False
if AliasEntry.objects.filter(name=name, entry__schema=self, entry__is_active=True).exists():
return False
return True
23 changes: 23 additions & 0 deletions entity/tests/test_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2961,6 +2961,29 @@ def test_create_entry(self):
search_result = self._es.search(body={"query": {"term": {"name": entry.name}}})
self.assertEqual(search_result["hits"]["total"]["value"], 1)

def test_create_entry_when_duplicated_alias_exists(self):
# make an Item and Alias to prevent to creating another Item
entry: Entry = self.add_entry(self.user, "Everest", self.entity)
entry.add_alias("Chomolungma")

resp = self.client.post(
"/entity/api/v2/%s/entries/" % self.entity.id,
json.dumps({"name": "Chomolungma"}),
"application/json",
)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
resp.json(),
{
"non_field_errors": [
{
"message": "A duplicated named Alias exists in this model",
"code": "AE-220000",
}
]
},
)

def test_create_entry_without_permission_entity(self):
params = {
"name": "entry1",
Expand Down
31 changes: 30 additions & 1 deletion entry/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from airone.lib.types import AttrDefaultValue, AttrType
from entity.api_v2.serializers import EntitySerializer
from entity.models import Entity, EntityAttr
from entry.models import Attribute, AttributeValue, Entry
from entry.models import AliasEntry, Attribute, AttributeValue, Entry
from entry.settings import CONFIG as CONFIG_ENTRY
from group.models import Group
from job.models import Job, JobStatus
Expand Down Expand Up @@ -214,6 +214,22 @@ class AttrTypeField(serializers.IntegerField):
schema = EntityAttributeTypeSerializer()


class EntryAliasSerializer(serializers.ModelSerializer):
class Meta:
model = AliasEntry
fields = [
"id",
"name",
"entry",
]

def validate(self, params):
if not params["entry"].schema.is_available(params["name"]):
raise DuplicatedObjectExistsError("A duplicated named Alias exists in this model")

return params


class EntryBaseSerializer(serializers.ModelSerializer):
# This attribute toggle privileged mode that allow user to CRUD Entry without
# considering permission. This must not change from program, but declare in a
Expand All @@ -222,6 +238,7 @@ class EntryBaseSerializer(serializers.ModelSerializer):

schema = EntitySerializer(read_only=True)
deleted_user = UserBaseSerializer(read_only=True, allow_null=True)
aliases = EntryAliasSerializer(many=True, read_only=True)

class Meta:
model = Entry
Expand All @@ -233,6 +250,7 @@ class Meta:
"deleted_user",
"deleted_time",
"updated_time",
"aliases",
]
extra_kwargs = {
"id": {"read_only": True},
Expand All @@ -245,12 +263,15 @@ def validate_name(self, name: str):
schema = self.instance.schema
else:
schema = self.get_initial()["schema"]

# Check there is another Item that has same name
if name and Entry.objects.filter(name=name, schema=schema, is_active=True).exists():
# In update case, there is no problem with the same name
if not (self.instance and self.instance.name == name):
raise DuplicatedObjectExistsError("specified name(%s) already exists" % name)
if "\t" in name:
raise InvalidValueError("Names containing tab characters cannot be specified.")

return name

def _validate(self, schema: Entity, name: str, attrs: list[dict[str, Any]]):
Expand All @@ -276,6 +297,11 @@ def _validate(self, schema: Entity, name: str, attrs: list[dict[str, Any]]):
"mandatory attrs id(%s) is not specified" % mandatory_attr.id
)

exclude_items = [self.instance.id] if self.instance else []
# Check there is another Alias that has same name
if not schema.is_available(name, exclude_items):
raise DuplicatedObjectExistsError("A duplicated named Alias exists in this model")

# check attrs
for attr in attrs:
# check attrs id
Expand All @@ -296,6 +322,9 @@ def _validate(self, schema: Entity, name: str, attrs: list[dict[str, Any]]):
"validate_entry", schema.name, user, schema.name, name, attrs, self.instance
)

def get_aliases(self, obj: Entry):
return obj.aliases.all()


@extend_schema_field({})
class AttributeValueField(serializers.Field):
Expand Down
34 changes: 33 additions & 1 deletion entry/api_v2/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,39 @@
"<int:pk>/histories/",
views.EntryAPI.as_view(
{
"get": "list",
"get": "list_histories",
}
),
),
path(
"<int:pk>/alias/",
views.EntryAPI.as_view(
{
"get": "list_alias",
}
),
),
path(
"alias/",
views.EntryAliasAPI.as_view(
{
"post": "create",
}
),
),
path(
"alias/bulk/",
views.EntryAliasAPI.as_view(
{
"post": "bulk_create",
}
),
),
path(
"alias/<int:pk>",
views.EntryAliasAPI.as_view(
{
"delete": "destroy",
}
),
),
Expand Down
Loading

0 comments on commit 28d8130

Please sign in to comment.