Skip to content

Commit 48fb156

Browse files
authored
Merge pull request #1106 from userlocalhost/feature/advanced_search/join_attrs
Added new feature that user can get referred Item's contents that an Item of advanced search result refers to from advanced search result page.
2 parents c749c69 + e7307bb commit 48fb156

19 files changed

+685
-64
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
## In development
44

55
### Added
6+
* Added new feature that user can get referred Item's contents that an Item
7+
of advanced search result refers to from advanced search result page.
8+
Contributed by @userlocalhost, @hinashi
69

710
### Changed
811

apiclient/typescript-fetch/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dmm-com/airone-apiclient-typescript-fetch",
3-
"version": "0.0.11",
3+
"version": "0.0.12",
44
"description": "AirOne APIv2 client in TypeScript",
55
"main": "src/autogenerated/index.ts",
66
"scripts": {

entity/api_v2/serializers.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ class EntityAttrSerializer(serializers.ModelSerializer):
194194

195195
class Meta:
196196
model = EntityAttr
197-
fields = ("id", "name", "type")
197+
fields = ("id", "name", "type", "referral")
198198

199199

200200
class EntitySerializer(serializers.ModelSerializer):
@@ -649,4 +649,4 @@ def _do_import(resource, iter_data: Any):
649649

650650

651651
class EntityAttrNameSerializer(serializers.ListSerializer):
652-
child = serializers.CharField()
652+
child = EntityAttrSerializer()

entity/api_v2/views.py

+23-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from distutils.util import strtobool
2+
from typing import Dict, List
23

34
from django.db.models import Count, F
45
from django.http import Http404
@@ -271,29 +272,35 @@ def get_queryset(self):
271272
entity_ids = list(filter(None, self.request.query_params.get("entity_ids", "").split(",")))
272273

273274
if len(entity_ids) == 0:
274-
return (
275-
EntityAttr.objects.filter(is_active=True)
276-
.values_list("name", flat=True)
277-
.order_by("name")
278-
.distinct()
279-
)
275+
return EntityAttr.objects.filter(is_active=True)
276+
280277
else:
281278
entities = Entity.objects.filter(id__in=entity_ids, is_active=True)
282279
if len(entity_ids) != len(entities):
283280
# the case invalid entity-id was specified
284281
raise ValidationError("Target Entity doesn't exist")
285282
else:
286-
return (
287-
# filter only names appear in all specified entities
288-
EntityAttr.objects.filter(parent_entity__in=entities, is_active=True)
289-
.values("name")
290-
.annotate(count=Count("name"))
291-
.filter(count=len(entity_ids))
292-
.values_list("name", flat=True)
293-
.order_by("name")
294-
)
283+
# filter only names appear in all specified entities
284+
return EntityAttr.objects.filter(parent_entity__in=entities, is_active=True)
295285

296286
def get(self, request: Request) -> Response:
297287
queryset = self.get_queryset()
288+
289+
entity_ids: List[int] = list(
290+
filter(None, self.request.query_params.get("entity_ids", "").split(","))
291+
)
292+
names_query = queryset.values("name")
293+
if len(entity_ids) > 0:
294+
names_query = names_query.annotate(count=Count("name")).filter(count=len(entity_ids))
295+
296+
# Compile each attribute of referrals by attribute name
298297
serializer: Serializer = self.get_serializer(queryset)
299-
return Response(serializer.data)
298+
results: Dict[str, Dict] = {}
299+
for attrname in names_query.values_list("name", flat=True):
300+
for attrinfo in [x for x in serializer.data if x["name"] == attrname]:
301+
if attrname in results:
302+
results[attrname]["referral"] += attrinfo["referral"]
303+
else:
304+
results[attrname] = attrinfo
305+
306+
return Response(results.values())

entity/tests/test_api_v2.py

+22-13
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from airone.lib import types as atype
1414
from airone.lib.log import Logger
1515
from airone.lib.test import AironeViewTest
16-
from airone.lib.types import AttrTypeArrStr, AttrTypeStr, AttrTypeText, AttrTypeValue
16+
from airone.lib.types import AttrType, AttrTypeArrStr, AttrTypeStr, AttrTypeText, AttrTypeValue
1717
from entity import tasks
1818
from entity.models import Entity, EntityAttr
1919
from entry.models import Entry
@@ -3465,18 +3465,23 @@ def test_get_entity_attr_names(self):
34653465
"test_entity1": ["foo", "bar", "fuga"],
34663466
"test_entity2": ["bar", "hoge", "fuga"],
34673467
}
3468-
for entity_name, attrnames in entity_info.items():
3468+
for i, (entity_name, attrnames) in enumerate(entity_info.items()):
34693469
entity = Entity.objects.create(name=entity_name, created_user=user)
34703470

3471-
for attrname in attrnames:
3472-
entity.attrs.add(
3473-
EntityAttr.objects.create(
3474-
name=attrname,
3475-
type=AttrTypeValue["string"],
3476-
created_user=user,
3477-
parent_entity=entity,
3478-
)
3471+
for j, attrname in enumerate(attrnames):
3472+
is_object = attrname == "bar"
3473+
attrtype = AttrType.OBJECT if is_object else AttrType.STRING
3474+
3475+
attr = EntityAttr.objects.create(
3476+
name=attrname,
3477+
type=attrtype.value,
3478+
created_user=user,
3479+
parent_entity=entity,
34793480
)
3481+
if is_object:
3482+
attr.referral.add(entity)
3483+
3484+
entity.attrs.add(attr)
34803485

34813486
self.ref_entity.delete()
34823487
self.entity.attrs.all().delete()
@@ -3488,12 +3493,16 @@ def test_get_entity_attr_names(self):
34883493
"/entity/api/v2/attrs?entity_ids=%s" % ",".join([str(x.id) for x in entities])
34893494
)
34903495
self.assertEqual(resp.status_code, 200)
3491-
self.assertEqual(resp.json(), sorted(["bar", "fuga"]))
3496+
self.assertEqual([x["name"] for x in resp.json()], sorted(["bar", "fuga"]))
3497+
self.assertEqual(
3498+
[x["referral"] for x in resp.json() if x["type"] == AttrType.OBJECT.value][0],
3499+
list(entities.values_list("id", flat=True)),
3500+
)
34923501

3493-
# get all
3502+
# get all attribute infomations are returned collectly
34943503
resp = self.client.get("/entity/api/v2/attrs")
34953504
self.assertEqual(resp.status_code, 200)
3496-
self.assertEqual(resp.json(), sorted(["foo", "bar", "hoge", "fuga"]))
3505+
self.assertEqual([x["name"] for x in resp.json()], ["foo", "bar", "fuga", "hoge"])
34973506

34983507
# invalid entity_id(s)
34993508
resp = self.client.get("/entity/api/v2/attrs?entity_ids=9999")

entry/api_v2/serializers.py

+7
Original file line numberDiff line numberDiff line change
@@ -1100,10 +1100,17 @@ def validate_filter_key(self, filter_key: int):
11001100
return filter_key
11011101

11021102

1103+
class AdvancedSearchJoinAttrInfoSerializer(serializers.Serializer):
1104+
name = serializers.CharField()
1105+
offset = serializers.IntegerField(default=0)
1106+
attrinfo = AdvancedSearchResultAttrInfoSerializer(many=True)
1107+
1108+
11031109
class AdvancedSearchSerializer(serializers.Serializer):
11041110
entities = serializers.ListField(child=serializers.IntegerField())
11051111
entry_name = serializers.CharField(allow_blank=True, default="")
11061112
attrinfo = AdvancedSearchResultAttrInfoSerializer(many=True)
1113+
join_attrs = AdvancedSearchJoinAttrInfoSerializer(many=True, required=False)
11071114
has_referral = serializers.BooleanField(default=False)
11081115
referral_name = serializers.CharField(required=False, allow_blank=True)
11091116
is_output_all = serializers.BooleanField(default=True)

entry/api_v2/views.py

+123-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
RequiredParameterError,
2323
YAMLParser,
2424
)
25-
from airone.lib.types import AttrTypeValue
25+
from airone.lib.types import AttrType, AttrTypeValue
2626
from entity.models import Entity, EntityAttr
2727
from entry.api_v2.pagination import EntryReferralPagination
2828
from entry.api_v2.serializers import (
@@ -229,6 +229,69 @@ def post(self, request: Request) -> Response:
229229
is_all_entities = serializer.validated_data["is_all_entities"]
230230
entry_limit = serializer.validated_data["entry_limit"]
231231
entry_offset = serializer.validated_data["entry_offset"]
232+
join_attrs = serializer.validated_data.get("join_attrs", [])
233+
234+
def _get_joined_resp(prev_results, join_attr):
235+
"""
236+
This is a helper method for join_attrs that will get specified attr values
237+
that prev_result's ones refer to.
238+
"""
239+
# set hint_entity_ids for joining Items search and get item names
240+
# that specified attribute refers
241+
item_names = []
242+
hint_entity_ids = []
243+
for result in prev_results:
244+
entity = Entity.objects.filter(id=result["entity"]["id"]).last()
245+
if entity is None:
246+
continue
247+
248+
attr = entity.attrs.filter(name=join_attr["name"], is_active=True).last()
249+
if attr is None:
250+
continue
251+
252+
if attr.type == AttrTypeValue["object"]:
253+
# set hint Model ID
254+
hint_entity_ids += [x.id for x in attr.referral.filter(is_active=True)]
255+
256+
# set Item name
257+
attrinfo = result["attrs"][join_attr["name"]]
258+
if attrinfo["value"]["name"] not in item_names:
259+
item_names.append(attrinfo["value"]["name"])
260+
261+
# set parameters to filter joining search results
262+
hint_attrs = []
263+
for attrinfo in join_attr.get("attrinfo", []):
264+
hint_attrs.append(
265+
{
266+
"name": attrinfo["name"],
267+
"keyword": attrinfo.get("keyword"),
268+
"filter_key": attrinfo.get("filter_key"),
269+
}
270+
)
271+
272+
# search Items from elasticsearch to join
273+
return (
274+
# This represents whether user want to narrow down results by keyword of joined attr
275+
any(
276+
[
277+
x.get("keyword") or x.get("filter_key", 0) > 0
278+
for x in join_attr.get("attrinfo", [])
279+
]
280+
),
281+
Entry.search_entries(
282+
request.user,
283+
hint_entity_ids=list(set(hint_entity_ids)), # this removes depulicated IDs
284+
hint_attrs=hint_attrs,
285+
limit=entry_limit,
286+
entry_name="|".join(item_names),
287+
hint_referral=None,
288+
is_output_all=is_output_all,
289+
hint_referral_entity_id=None,
290+
offset=join_attr.get("offset", 0),
291+
),
292+
)
293+
294+
# === End of Function: _get_joined_resp() ===
232295

233296
if not has_referral:
234297
hint_referral = None
@@ -273,6 +336,65 @@ def post(self, request: Request) -> Response:
273336
offset=entry_offset,
274337
)
275338

339+
for join_attr in join_attrs:
340+
(will_filter_by_joined_attr, joined_resp) = _get_joined_resp(
341+
resp["ret_values"], join_attr
342+
)
343+
344+
# This is needed to set result as blank value
345+
blank_joining_info = {
346+
"%s.%s" % (join_attr["name"], k["name"]): {
347+
"is_readable": True,
348+
"type": AttrType.STRING.value,
349+
"value": "",
350+
}
351+
for k in join_attr["attrinfo"]
352+
}
353+
354+
# convert search result to dict to be able to handle it without loop
355+
joined_resp_info = {
356+
x["entry"]["id"]: {
357+
"%s.%s" % (join_attr["name"], k): v
358+
for k, v in x["attrs"].items()
359+
if any(_x["name"] == k for _x in join_attr["attrinfo"])
360+
}
361+
for x in joined_resp["ret_values"]
362+
}
363+
364+
# this inserts result to previous search result
365+
new_ret_values = []
366+
for resp_result in resp["ret_values"]:
367+
ref_info = resp_result["attrs"].get(join_attr["name"])
368+
if (
369+
# ignore no joined data
370+
ref_info is None
371+
or
372+
# ignore unexpected typed attributes
373+
ref_info["type"] != AttrType.OBJECT.value
374+
or
375+
# ignore when original result doesn't refer any item
376+
ref_info["value"].get("id") is None
377+
):
378+
# join EMPTY value
379+
resp_result["attrs"] |= blank_joining_info # type: ignore
380+
381+
# joining search result to original one
382+
ref_id = ref_info["value"].get("id") if "value" in ref_info is not None else None # type: ignore
383+
if ref_id and ref_id in joined_resp_info: # type: ignore
384+
# join valid value
385+
resp_result["attrs"] |= joined_resp_info[ref_id]
386+
387+
# collect only the result that matches with keyword of joined_attr parameter
388+
new_ret_values.append(resp_result)
389+
390+
else:
391+
# join EMPTY value
392+
resp_result["attrs"] |= blank_joining_info # type: ignore
393+
394+
if will_filter_by_joined_attr:
395+
resp["ret_values"] = new_ret_values
396+
resp["ret_count"] = len(new_ret_values)
397+
276398
# convert field values to fit entry retrieve API data type, as a workaround.
277399
# FIXME should be replaced with DRF serializer etc
278400
for entry in resp["ret_values"]:

0 commit comments

Comments
 (0)