Skip to content

Commit

Permalink
OpenConceptLab/ocl_issues#799 | apis to get hierarchy from source or …
Browse files Browse the repository at this point in the history
…concept
  • Loading branch information
snyaggarwal committed Jun 18, 2021
1 parent fa085fa commit bf495a0
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 9 deletions.
1 change: 1 addition & 0 deletions core/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
INCLUDE_EXTRAS_PARAM = 'includeExtras'
INCLUDE_PARENT_CONCEPTS = 'includeParentConcepts'
INCLUDE_CHILD_CONCEPTS = 'includeChildConcepts'
INCLUDE_HIERARCHY_PATH = 'includeHierarchyPath'
INCLUDE_INVERSE_MAPPINGS_PARAM = 'includeInverseMappings'
INCLUDE_SUBSCRIBED_ORGS = 'includeSubscribedOrgs'
INCLUDE_CLIENT_CONFIGS = 'includeClientConfigs'
Expand Down
16 changes: 16 additions & 0 deletions core/concepts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,22 @@ def child_concept_urls(self):
queryset |= self.get_latest_version().child_concepts.all()
return self.__format_hierarchy_uris(queryset.values_list('uri', flat=True))

def child_concept_queryset(self):
urls = self.child_concept_urls
if urls:
return Concept.objects.filter(uri__in=urls)
return Concept.objects.none()

@staticmethod
def __format_hierarchy_uris(uris):
return list({drop_version(uri) for uri in uris})

def get_hierarchy_path(self):
result = []
parent_concept = self.parent_concepts.first()
while parent_concept is not None:
result.append(drop_version(parent_concept.uri))
parent_concept = parent_concept.parent_concepts.first()

result.reverse()
return result
23 changes: 21 additions & 2 deletions core/concepts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from core.common.constants import INCLUDE_INVERSE_MAPPINGS_PARAM, INCLUDE_MAPPINGS_PARAM, INCLUDE_EXTRAS_PARAM, \
INCLUDE_PARENT_CONCEPTS, INCLUDE_CHILD_CONCEPTS, INCLUDE_SOURCE_VERSIONS, INCLUDE_COLLECTION_VERSIONS, \
CREATE_PARENT_VERSION_QUERY_PARAM
CREATE_PARENT_VERSION_QUERY_PARAM, INCLUDE_HIERARCHY_PATH
from core.common.fields import EncodedDecodedCharField
from core.concepts.models import Concept, LocalizedText

Expand Down Expand Up @@ -208,6 +208,7 @@ class ConceptDetailSerializer(ModelSerializer):
mappings = SerializerMethodField()
parent_concepts = SerializerMethodField()
child_concepts = SerializerMethodField()
hierarchy_path = SerializerMethodField()

def __init__(self, *args, **kwargs):
params = get(kwargs, 'context.request.query_params')
Expand All @@ -216,6 +217,7 @@ def __init__(self, *args, **kwargs):
self.include_direct_mappings = self.query_params.get(INCLUDE_MAPPINGS_PARAM) in ['true', True]
self.include_parent_concepts = self.query_params.get(INCLUDE_PARENT_CONCEPTS) in ['true', True]
self.include_child_concepts = self.query_params.get(INCLUDE_CHILD_CONCEPTS) in ['true', True]
self.include_hierarchy_path = self.query_params.get(INCLUDE_HIERARCHY_PATH) in ['true', True]
if CREATE_PARENT_VERSION_QUERY_PARAM in self.query_params:
self.create_parent_version = self.query_params.get(CREATE_PARENT_VERSION_QUERY_PARAM) in ['true', True]
else:
Expand All @@ -238,7 +240,7 @@ class Meta:
'owner', 'owner_type', 'owner_url', 'display_name', 'display_locale', 'names', 'descriptions',
'created_on', 'updated_on', 'versions_url', 'version', 'extras', 'parent_id', 'name', 'type',
'update_comment', 'version_url', 'mappings', 'updated_by', 'created_by', 'internal_reference_id',
'parent_concept_urls', 'child_concept_urls', 'parent_concepts', 'child_concepts'
'parent_concept_urls', 'child_concept_urls', 'parent_concepts', 'child_concepts', 'hierarchy_path'
)

def get_mappings(self, obj):
Expand Down Expand Up @@ -279,6 +281,11 @@ def get_parent_concepts(self, obj):
return ConceptDetailSerializer(obj.parent_concepts.all(), many=True).data
return None

def get_hierarchy_path(self, obj):
if self.include_hierarchy_path:
return obj.get_hierarchy_path()
return None


class ConceptVersionDetailSerializer(ModelSerializer):
type = CharField(source='resource_type')
Expand Down Expand Up @@ -356,3 +363,15 @@ def get_parent_concepts(self, obj):
if self.include_parent_concepts:
return ConceptDetailSerializer(obj.parent_concepts.all(), many=True).data
return None


class ConceptHierarchySerializer(ModelSerializer):
uuid = CharField(source='id')
id = EncodedDecodedCharField(source='mnemonic')
url = CharField(source='uri')
children = ListField(source='child_concept_urls')
name = CharField(source='display_name')

class Meta:
model = Concept
fields = ('uuid', 'id', 'url', 'children', 'name')
52 changes: 52 additions & 0 deletions core/concepts/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,58 @@ def test_get_parent_and_owner_filters_from_uri(self):
dict(parent__mnemonic='bar', parent__organization__mnemonic='foo')
)

def test_get_hierarchy_path(self):
parent_concept = ConceptFactory()
self.assertEqual(parent_concept.get_hierarchy_path(), [])

child_concept = Concept.persist_new({
**factory.build(dict, FACTORY_CLASS=ConceptFactory), 'mnemonic': 'c1', 'parent': parent_concept.parent,
'names': [LocalizedTextFactory.build(locale='en', name='English', locale_preferred=True)],
'parent_concept_urls': [parent_concept.uri]
})

self.assertEqual(parent_concept.get_hierarchy_path(), [])
self.assertEqual(child_concept.get_hierarchy_path(), [parent_concept.uri])

child_child_concept = Concept.persist_new({
**factory.build(dict, FACTORY_CLASS=ConceptFactory), 'mnemonic': 'c2', 'parent': parent_concept.parent,
'names': [LocalizedTextFactory.build(locale='en', name='English', locale_preferred=True)],
'parent_concept_urls': [child_concept.uri]
})

self.assertEqual(parent_concept.get_hierarchy_path(), [])
self.assertEqual(child_concept.get_hierarchy_path(), [parent_concept.uri])
self.assertEqual(child_child_concept.get_hierarchy_path(), [parent_concept.uri, child_concept.uri])

def test_child_concept_queryset(self):
parent_concept = ConceptFactory()
self.assertEqual(parent_concept.child_concept_queryset().count(), 0)
self.assertEqual(parent_concept.parent_concept_urls, [])

child_concept = Concept.persist_new({
**factory.build(dict, FACTORY_CLASS=ConceptFactory), 'mnemonic': 'c1', 'parent': parent_concept.parent,
'names': [LocalizedTextFactory.build(locale='en', name='English', locale_preferred=True)],
'parent_concept_urls': [parent_concept.uri]
})
self.assertEqual(
list(parent_concept.child_concept_queryset().values_list('uri', flat=True)), [child_concept.uri])
self.assertEqual(
list(child_concept.child_concept_queryset().values_list('uri', flat=True)), [])
self.assertEqual(child_concept.parent_concept_urls, [parent_concept.uri])

child_child_concept = Concept.persist_new({
**factory.build(dict, FACTORY_CLASS=ConceptFactory), 'mnemonic': 'c2', 'parent': parent_concept.parent,
'names': [LocalizedTextFactory.build(locale='en', name='English', locale_preferred=True)],
'parent_concept_urls': [child_concept.uri]
})
self.assertEqual(
list(parent_concept.child_concept_queryset().values_list('uri', flat=True)), [child_concept.uri])
self.assertEqual(
list(child_concept.child_concept_queryset().values_list('uri', flat=True)), [child_child_concept.uri])
self.assertEqual(
list(child_child_concept.child_concept_queryset().values_list('uri', flat=True)), [])
self.assertEqual(child_child_concept.parent_concept_urls, [child_concept.uri])


class OpenMRSConceptValidatorTest(OCLTestCase):
def setUp(self):
Expand Down
5 changes: 5 additions & 0 deletions core/concepts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
views.ConceptReactivateView.as_view(),
name='concept-reactivate'
),
re_path(
r"^(?P<concept>{pattern})/children/$".format(pattern=NAMESPACE_PATTERN),
views.ConceptChildrenView.as_view(),
name='concept-children'
),
re_path(r'^(?P<concept>{pattern})/atom/$'.format(pattern=NAMESPACE_PATTERN), ConceptFeed()),
re_path(
r"^(?P<concept>{pattern})/descriptions/$".format(pattern=NAMESPACE_PATTERN),
Expand Down
13 changes: 11 additions & 2 deletions core/concepts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pydash import get
from rest_framework import status
from rest_framework.generics import RetrieveAPIView, DestroyAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView, \
UpdateAPIView
UpdateAPIView, ListAPIView
from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAdminUser
from rest_framework.response import Response
Expand All @@ -28,7 +28,7 @@
from core.concepts.serializers import (
ConceptDetailSerializer, ConceptListSerializer, ConceptDescriptionSerializer, ConceptNameSerializer,
ConceptVersionDetailSerializer,
ConceptVersionListSerializer)
ConceptVersionListSerializer, ConceptHierarchySerializer)
from core.mappings.serializers import MappingListSerializer


Expand Down Expand Up @@ -207,6 +207,15 @@ def destroy(self, request, *args, **kwargs):
return Response(status=status.HTTP_204_NO_CONTENT)


class ConceptChildrenView(ConceptBaseView, ListAPIView):
serializer_class = ConceptHierarchySerializer

def get_queryset(self):
instance = get_object_or_404(super().get_queryset(), id=F('versioned_object_id'))
self.check_object_permissions(self.request, instance)
return instance.child_concept_queryset()


class ConceptReactivateView(ConceptBaseView, UpdateAPIView):
serializer_class = ConceptDetailSerializer

Expand Down
7 changes: 5 additions & 2 deletions core/integration_tests/tests_concepts.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def test_post_201(self):
'internal_reference_id',
'parent_concept_urls',
'child_concept_urls',
'hierarchy_path',
]
)

Expand Down Expand Up @@ -199,7 +200,8 @@ def test_put_200(self):
'created_by',
'internal_reference_id',
'parent_concept_urls',
'child_concept_urls']
'child_concept_urls',
'hierarchy_path']
)

version = Concept.objects.last()
Expand Down Expand Up @@ -276,7 +278,8 @@ def test_put_200_openmrs_schema(self): # pylint: disable=too-many-statements
'created_by',
'internal_reference_id',
'parent_concept_urls',
'child_concept_urls']
'child_concept_urls',
'hierarchy_path']
)

names = response.data['names']
Expand Down
25 changes: 24 additions & 1 deletion core/sources/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import UniqueConstraint
from django.db.models import UniqueConstraint, F
from django.urls import resolve
from pydash import get, compact

Expand Down Expand Up @@ -149,3 +149,26 @@ def is_hierarchy_root_belonging_to_self(self):
def clean(self):
if self.hierarchy_root_id and not self.is_hierarchy_root_belonging_to_self():
raise ValidationError({'hierarchy_root': [HIERARCHY_ROOT_MUST_BELONG_TO_SAME_SOURCE]})

def get_parentless_concepts(self):
return self.concepts.filter(parent_concepts__isnull=True, id=F('versioned_object_id'))

def hierarchy(self):
from core.concepts.serializers import ConceptHierarchySerializer
hierarchy_root = self.hierarchy_root
parent_less_children = self.get_parentless_concepts()
if hierarchy_root:
parent_less_children = parent_less_children.exclude(mnemonic=hierarchy_root.mnemonic)

total_count = parent_less_children.count()
parent_less_children = parent_less_children.order_by('mnemonic')[0:100]
children = ConceptHierarchySerializer(compact(parent_less_children), many=True).data

if hierarchy_root:
children.append({**ConceptHierarchySerializer(hierarchy_root).data, 'root': True})

return dict(
id=self.mnemonic,
children=children,
count=total_count + (1 if hierarchy_root else 0)
)
60 changes: 58 additions & 2 deletions core/sources/tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import factory
from django.core.exceptions import ValidationError
from django.db import transaction, IntegrityError
from mock import patch, Mock
from mock import patch, Mock, ANY

from core.common.constants import HEAD
from core.common.tasks import seed_children
from core.common.tests import OCLTestCase
from core.concepts.tests.factories import ConceptFactory
from core.concepts.models import Concept
from core.concepts.tests.factories import ConceptFactory, LocalizedTextFactory
from core.mappings.tests.factories import MappingFactory
from core.sources.models import Source
from core.sources.tests.factories import OrganizationSourceFactory
Expand Down Expand Up @@ -474,6 +476,60 @@ def test_hierarchy_root(self):
source.hierarchy_root = source_concept
source.full_clean()

def test_hierarchy_with_hierarchy_root(self):
source = OrganizationSourceFactory()
root_concept = ConceptFactory(parent=source, mnemonic='root')
source.hierarchy_root = root_concept
source.save()
child_concept = Concept.persist_new({
**factory.build(dict, FACTORY_CLASS=ConceptFactory), 'mnemonic': 'root-kid',
'parent': source,
'names': [LocalizedTextFactory.build(locale='en', name='English', locale_preferred=True)],
'parent_concept_urls': [root_concept.uri]
})
parentless_concept = ConceptFactory(parent=source, mnemonic='parentless')
parentless_concept_child = Concept.persist_new({
**factory.build(dict, FACTORY_CLASS=ConceptFactory), 'mnemonic': 'parentless-kid',
'parent': source,
'names': [LocalizedTextFactory.build(locale='en', name='English', locale_preferred=True)],
'parent_concept_urls': [parentless_concept.uri]
})

hierarchy = source.hierarchy()
self.assertEqual(hierarchy, dict(id=source.mnemonic, count=2, children=ANY))
hierarchy_children = hierarchy['children']
self.assertEqual(len(hierarchy_children), 2)
self.assertEqual(
hierarchy_children[1],
dict(uuid=str(root_concept.id), id=root_concept.mnemonic, url=root_concept.uri,
name=root_concept.display_name, children=[child_concept.uri], root=True)
)
self.assertEqual(
hierarchy_children[0],
dict(uuid=str(parentless_concept.id), id=parentless_concept.mnemonic, url=parentless_concept.uri,
name=parentless_concept.display_name, children=[parentless_concept_child.uri])
)

def test_hierarchy_without_hierarchy_root(self):
source = OrganizationSourceFactory()
parentless_concept = ConceptFactory(parent=source, mnemonic='parentless')
parentless_concept_child = Concept.persist_new({
**factory.build(dict, FACTORY_CLASS=ConceptFactory), 'mnemonic': 'parentless-kid',
'parent': source,
'names': [LocalizedTextFactory.build(locale='en', name='English', locale_preferred=True)],
'parent_concept_urls': [parentless_concept.uri]
})

hierarchy = source.hierarchy()
self.assertEqual(hierarchy, dict(id=source.mnemonic, count=1, children=ANY))
hierarchy_children = hierarchy['children']
self.assertEqual(len(hierarchy_children), 1)
self.assertEqual(
hierarchy_children[0],
dict(uuid=str(parentless_concept.id), id=parentless_concept.mnemonic, url=parentless_concept.uri,
name=parentless_concept.display_name, children=[parentless_concept_child.uri])
)


class TasksTest(OCLTestCase):
@patch('core.common.models.ConceptContainerModel.index_children')
Expand Down
5 changes: 5 additions & 0 deletions core/sources/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
views.SourceSummaryView.as_view(),
name='source-summary'
),
re_path(
r"^(?P<source>{pattern})/hierarchy/$".format(pattern=NAMESPACE_PATTERN),
views.SourceHierarchyView.as_view(),
name='source-hierarchy'
),
re_path(
r"^(?P<source>{pattern})/logo/$".format(pattern=NAMESPACE_PATTERN),
views.SourceLogoView.as_view(),
Expand Down
14 changes: 14 additions & 0 deletions core/sources/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,20 @@ def handle_export_version(self):
return status.HTTP_409_CONFLICT


class SourceHierarchyView(SourceBaseView, RetrieveAPIView):
serializer_class = SourceSummaryDetailSerializer
permission_classes = (CanViewConceptDictionary,)

def get_object(self, queryset=None):
instance = get_object_or_404(self.get_queryset())
self.check_object_permissions(self.request, instance)
return instance

def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
return Response(instance.hierarchy())


class SourceSummaryView(SourceBaseView, RetrieveAPIView):
serializer_class = SourceSummaryDetailSerializer
permission_classes = (CanViewConceptDictionary,)
Expand Down

0 comments on commit bf495a0

Please sign in to comment.