Skip to content

Commit

Permalink
Merge pull request #1003 from hinashi/feature/airone_trigger
Browse files Browse the repository at this point in the history
Added trigger action model
  • Loading branch information
hinashi authored Feb 1, 2024
2 parents deb02aa + e558964 commit da429e6
Show file tree
Hide file tree
Showing 39 changed files with 3,551 additions and 36 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## In development

### Added
* Added new feature TriggerAction to be able to update Attributes along with user defined configuration.
Contributed by @userlocalhsot, @hinashi

### Changed

Expand Down
5 changes: 5 additions & 0 deletions airone/exceptions/trigger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from rest_framework.exceptions import ValidationError


class InvalidInputException(ValidationError):
default_code = "AE-300000"
1 change: 1 addition & 0 deletions airone/settings_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class Common(Configuration):
"social_django",
"simple_history",
"storages",
"trigger",
]

MIDDLEWARE = [
Expand Down
1 change: 1 addition & 0 deletions airone/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
url(r"^auth/logout/", auth_view.logout, name="logout"),
url(r"^webhook/", include(("webhook.urls", "webhook"))),
url(r"^role/", include(("role.urls", "role"))),
url(r"^trigger/", include(("trigger.urls", "trigger"))),
]

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.0.8",
"version": "0.0.9",
"description": "AirOne APIv2 client in TypeScript",
"main": "src/autogenerated/index.ts",
"scripts": {
Expand Down
8 changes: 8 additions & 0 deletions entity/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ class EntityUpdateData(TypedDict, total=False):
webhooks: WebhookCreateUpdateSerializer


class EntityAttrSerializer(serializers.ModelSerializer):
type = serializers.IntegerField(required=False, read_only=True)

class Meta:
model = EntityAttr
fields = ("id", "name", "type")


class EntitySerializer(serializers.ModelSerializer):
class Meta:
model = Entity
Expand Down
41 changes: 41 additions & 0 deletions entity/tests/test_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from entry.models import Entry
from group.models import Group
from role.models import Role
from trigger import tasks as trigger_tasks
from trigger.models import TriggerCondition
from user.models import History, User
from webhook.models import Webhook

Expand Down Expand Up @@ -3462,3 +3464,42 @@ def test_get_entity_attr_names(self):
# invalid entity_id(s)
resp = self.client.get("/entity/api/v2/attrs?entity_ids=9999")
self.assertEqual(resp.status_code, 400)

@mock.patch(
"trigger.tasks.may_invoke_trigger.delay",
mock.Mock(side_effect=trigger_tasks.may_invoke_trigger),
)
def test_create_entry_when_trigger_is_set(self):
attr = {}
for attr_name in [x["name"] for x in self.ALL_TYPED_ATTR_PARAMS_FOR_CREATING_ENTITY]:
attr[attr_name] = self.entity.attrs.get(name=attr_name)

# register Trigger and Action that specify "fuga" at text attribute
# when value "hoge" is set to the Attribute "val".
TriggerCondition.register(
self.entity,
[{"attr_id": attr["val"].id, "cond": "hoge"}],
[{"attr_id": attr["vals"].id, "values": ["fuga", "piyo"]}],
)
TriggerCondition.register(
self.entity,
[{"attr_id": attr["vals"].id, "cond": "fuga"}],
[{"attr_id": attr["text"].id, "value": "hogefuga"}],
)

# send request to create an Entry that have "hoge" at the Attribute "val".
params = {
"name": "entry1",
"attrs": [{"id": attr["val"].id, "value": "hoge"}],
}
resp = self.client.post(
"/entity/api/v2/%s/entries/" % self.entity.id, json.dumps(params), "application/json"
)
self.assertEqual(resp.status_code, 201)

# check Attribute "vals", which is specified by TriggerCondition, was changed as expected
entry: Entry = Entry.objects.get(id=resp.json()["id"], is_active=True)
self.assertEqual(entry.get_attrv("text").value, "hogefuga")
self.assertEqual(
[x.value for x in entry.get_attrv("vals").data_array.all()], ["fuga", "piyo"]
)
26 changes: 25 additions & 1 deletion entry/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ def create(self, validated_data: EntryCreateData):
# register entry information to Elasticsearch
entry.register_es()

# run task that may run TriggerAction in response to TriggerCondition configuration
Job.new_invoke_trigger(user, entry, attrs_data).run()

# clear flag to specify this entry has been completed to create
entry.del_status(Entry.STATUS_CREATING)

Expand All @@ -310,14 +313,22 @@ def create(self, validated_data: EntryCreateData):
class EntryUpdateData(TypedDict, total=False):
name: str
attrs: list[AttributeDataSerializer]
delay_trigger: bool
call_stacks: list[int]


class EntryUpdateSerializer(EntryBaseSerializer):
attrs = serializers.ListField(child=AttributeDataSerializer(), write_only=True, required=False)

# These parameters are only used to run TriggerActions
delay_trigger = serializers.BooleanField(required=False, default=True)
# This will contain EntityAttr IDs that have already been updated in this TriggerAction
# running chain.
call_stacks = serializers.ListField(child=serializers.IntegerField(), required=False)

class Meta:
model = Entry
fields = ["id", "name", "attrs"]
fields = ["id", "name", "attrs", "delay_trigger", "call_stacks"]
extra_kwargs = {
"name": {"required": False},
}
Expand Down Expand Up @@ -379,6 +390,19 @@ def update(self, entry: Entry, validated_data: EntryUpdateData):
if is_updated:
entry.register_es()

# run task that may run TriggerAction in response to TriggerCondition configuration
if validated_data["delay_trigger"]:
Job.new_invoke_trigger(user, entry, attrs_data).run()
else:
# This declaration prevents circular reference because TriggerAction module
# imports this module indirectly. And this might affect little negative affect
# because Python interpreter will cache imported module once it's imported.
from trigger.models import TriggerCondition

# run TriggerActions immediately if it's necessary
for action in TriggerCondition.get_invoked_actions(entry.schema, attrs_data):
action.run(user, entry, validated_data["call_stacks"])

# clear flag to specify this entry has been completed to edit
entry.del_status(Entry.STATUS_EDITING)

Expand Down
26 changes: 23 additions & 3 deletions entry/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,11 @@ def clone(self, user, **extra_params):
return cloned_value

def get_value(
self, with_metainfo=False, with_entity: bool = False, serialize=False, is_active=True
self,
with_metainfo=False,
with_entity: bool = False,
serialize=False,
is_active=True,
):
"""
This returns registered value according to the type of Attribute
Expand Down Expand Up @@ -365,6 +369,13 @@ def _is_validate_attr_str(value) -> bool:

def _is_validate_attr_object(value) -> bool:
try:
if isinstance(value, Entry) and value.is_active:
return True
if (
isinstance(value, ACLBase)
and Entry.objects.filter(id=value.id, is_active=True).exists()
):
raise Exception("value(%s) is not valid entry" % value.name)
if value and not Entry.objects.filter(id=value, is_active=True).exists():
raise Exception("value(%s) is not entry id" % value)
if is_mandatory and not value:
Expand Down Expand Up @@ -1889,7 +1900,11 @@ def _set_attrinfo_data(model):
"name": self.name,
"attr": [],
"referrals": [
{"id": x.id, "name": x.name, "schema": {"id": x.schema.id, "name": x.schema.name}}
{
"id": x.id,
"name": x.name,
"schema": {"id": x.schema.id, "name": x.schema.name},
}
for x in self.get_referred_objects().select_related("schema")
],
"is_readable": True
Expand Down Expand Up @@ -2234,7 +2249,12 @@ def get_all_es_docs(kls):
def update_documents(kls, entity: Entity, is_update: bool = False):
es = ESS()
query = {
"query": {"nested": {"path": "entity", "query": {"match": {"entity.id": entity.id}}}}
"query": {
"nested": {
"path": "entity",
"query": {"match": {"entity.id": entity.id}},
}
}
}
res = es.search(body=query)

Expand Down
43 changes: 43 additions & 0 deletions entry/tests/test_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
from group.models import Group
from job.models import Job, JobOperation
from role.models import Role
from trigger import tasks as trigger_tasks
from trigger.models import TriggerCondition
from user.models import User


Expand Down Expand Up @@ -703,6 +705,7 @@ def test_update_entry(self):
{
"id": entry.id,
"name": "entry-change",
"delay_trigger": True,
},
)
self.assertEqual(entry.status, 0)
Expand Down Expand Up @@ -1043,6 +1046,7 @@ def test_update_entry_with_customview(self, mock_call_custom):
"attrs": [
{"id": attr["val"].id, "value": "fuga"},
],
"delay_trigger": True,
}

def side_effect(handler_name, entity_name, user, *args):
Expand Down Expand Up @@ -4532,3 +4536,42 @@ def test_destroy_entries_notify(self, mock_task):
self.client.delete("/entry/api/v2/bulk_delete/?ids=%s" % entry.id, None, "application/json")

self.assertTrue(mock_task.called)

@mock.patch(
"trigger.tasks.may_invoke_trigger.delay",
mock.Mock(side_effect=trigger_tasks.may_invoke_trigger),
)
def test_update_entry_when_trigger_is_set(self):
# create Entry to be updated in this test
entry: Entry = self.add_entry(self.user, "entry", self.entity)

attr = {}
for attr_name in [x["name"] for x in self.ALL_TYPED_ATTR_PARAMS_FOR_CREATING_ENTITY]:
attr[attr_name] = self.entity.attrs.get(name=attr_name)

# register Trigger and Action that specify "fuga" at text attribute
# when value "hoge" is set to the Attribute "val".
TriggerCondition.register(
self.entity,
[
{"attr_id": self.entity.attrs.get(name="val").id, "cond": "hoge"},
],
[
{"attr_id": self.entity.attrs.get(name="text").id, "value": "fuga"},
],
)

# send request to update Entry
params = {
"name": "entry-change",
"attrs": [
{"id": attr["val"].id, "value": "hoge"},
],
}
resp = self.client.put(
"/entry/api/v2/%s/" % entry.id, json.dumps(params), "application/json"
)
self.assertEqual(resp.status_code, 200)

# check Attribute "text", which is specified by TriggerCondition, was changed to "fuga"
self.assertEqual(entry.get_attrv("text").value, "fuga")
35 changes: 23 additions & 12 deletions frontend/src/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { RouteComponentProps } from "react-router";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { Route, BrowserRouter as Router, Switch } from "react-router-dom";

import { ErrorHandler } from "./ErrorHandler";
import { ACLHistoryPage } from "./pages/ACLHistoryPage";
Expand All @@ -12,33 +12,36 @@ import { RestoreEntryPage } from "./pages/RestoreEntryPage";
import { RolePage } from "./pages/RolePage";

import {
aclHistoryPath,
aclPath,
advancedSearchPath,
advancedSearchResultPath,
copyEntryPath,
editEntityPath,
editTriggerPath,
entitiesPath,
entityEntriesPath,
entityHistoryPath,
editEntityPath,
entryDetailsPath,
entryEditPath,
groupPath,
groupsPath,
jobsPath,
loginPath,
newEntityPath,
newEntryPath,
newGroupPath,
newRolePath,
newTriggerPath,
newUserPath,
userPath,
usersPath,
loginPath,
showEntryHistoryPath,
entryEditPath,
entryDetailsPath,
copyEntryPath,
restoreEntryPath,
rolesPath,
rolePath,
newRolePath,
rolesPath,
showEntryHistoryPath,
topPath,
aclHistoryPath,
triggersPath,
userPath,
usersPath,
} from "Routes";
import { Header } from "components/Header";
import { ACLPage } from "pages/ACLPage";
Expand All @@ -47,6 +50,7 @@ import { AdvancedSearchResultsPage } from "pages/AdvancedSearchResultsPage";
import { DashboardPage } from "pages/DashboardPage";
import { EditEntityPage } from "pages/EditEntityPage";
import { EditGroupPage } from "pages/EditGroupPage";
import { EditTriggerPage } from "pages/EditTriggerPage";
import { EditUserPage } from "pages/EditUserPage";
import { EntityHistoryPage } from "pages/EntityHistoryPage";
import { EntityListPage } from "pages/EntityListPage";
Expand All @@ -56,6 +60,7 @@ import { EntryListPage } from "pages/EntryListPage";
import { GroupPage } from "pages/GroupPage";
import { JobPage } from "pages/JobPage";
import { LoginPage } from "pages/LoginPage";
import { TriggerPage } from "pages/TriggerPage";
import { UserPage } from "pages/UserPage";

interface Props {
Expand Down Expand Up @@ -137,6 +142,12 @@ export const AppRouter: FC<Props> = ({ customRoutes }) => {
component={EditEntityPage}
/>
<Route path={entitiesPath()} component={EntityListPage} />
<Route path={newTriggerPath()} component={EditTriggerPage} />
<Route
path={editTriggerPath(":triggerId")}
component={EditTriggerPage}
/>
<Route path={triggersPath()} component={TriggerPage} />
<Route path={newGroupPath()} component={EditGroupPage} />
<Route path={groupPath(":groupId")} component={EditGroupPage} />
<Route path={groupsPath()} component={GroupPage} />
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/Routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export const editEntityPath = (entityId: number | string) =>
basePath + `entities/${entityId}`;
export const entitiesPath = () => basePath + "entities";

// triggers
export const newTriggerPath = () => basePath + "triggers/new";
export const editTriggerPath = (triggerId: number | string) =>
basePath + `triggers/${triggerId}`;
export const triggersPath = () => basePath + "triggers";

// groups
export const newGroupPath = () => basePath + "groups/new";
export const groupPath = (groupId: number | string) =>
Expand Down
Loading

0 comments on commit da429e6

Please sign in to comment.