Skip to content

Commit aff8c42

Browse files
ieuansJackScanlonZinnurovArturroshantoby
authored
feat: Feat/hdrn (#1790)
Co-authored-by: ieuans <ieuanscanlon@hotmail.co.uk> Co-authored-by: JackScanlon <jackascanlon@hotmail.co.uk> Co-authored-by: Arthur zinnurov <zinnurov_2012@mail.ru> Co-authored-by: roshantoby <roshantoby@gmail.com>
1 parent 09db2a2 commit aff8c42

File tree

366 files changed

+40104
-10660
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

366 files changed

+40104
-10660
lines changed

.github/workflows/testing-pipline.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,8 @@ jobs:
4343
steps:
4444
- uses: actions/checkout@v3
4545
- run: |
46-
sudo apt-get update
47-
sudo apt-get install -y libgconf-2-4 libatk1.0-0 libatk-bridge2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libgbm-dev libnss3-dev libxss-dev libasound2
48-
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
46+
sudo apt-get update
47+
sudo apt-get install -y libgconf-2-4 libatk1.0-0 libatk-bridge2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libgbm-dev libnss3-dev libxss-dev libasound2
4948
- uses: browser-actions/setup-chrome@v1
5049
- uses: actions/cache@v3
5150
with:
@@ -65,6 +64,7 @@ jobs:
6564
6665
- name: Install Dependencies
6766
run: |
67+
sudo apt-get install -y -q dirmngr
6868
python -m pip install --upgrade pip
6969
pip install --upgrade --upgrade-strategy eager --default-timeout 100 -r docker/requirements/test.txt
7070

CodeListLibrary_project/clinicalcode/admin.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
from .models.GenericEntity import GenericEntity
99
from .models.Template import Template
1010
from .models.OntologyTag import OntologyTag
11+
from .models.Organisation import Organisation
1112
from .models.DMD_CODES import DMD_CODES
13+
1214
from .forms.TemplateForm import TemplateAdminForm
1315
from .forms.EntityClassForm import EntityAdminForm
16+
from .forms.OrganisationForms import OrganisationAdminForm, OrganisationMembershipInline, OrganisationAuthorityInline
1417

1518
@admin.register(OntologyTag)
1619
class OntologyTag(admin.ModelAdmin):
@@ -51,10 +54,9 @@ def save_model(self, request, obj, form, change):
5154

5255
@admin.register(Brand)
5356
class BrandAdmin(admin.ModelAdmin):
54-
list_display = ['name', 'id', 'logo_path', 'owner', 'description']
55-
list_filter = ['name', 'description', 'created', 'modified', 'owner']
57+
list_filter = ['name', 'description', 'created', 'modified']
58+
list_display = ['name', 'id', 'logo_path', 'description']
5659
search_fields = ['name', 'id', 'description']
57-
exclude = ['created_by', 'updated_by']
5860

5961

6062
@admin.register(DataSource)
@@ -72,6 +74,47 @@ class CodingSystemAdmin(admin.ModelAdmin):
7274
search_fields = ['name', 'codingsystem_id', 'description']
7375
exclude = []
7476

77+
@admin.register(Organisation)
78+
class OrganisationAdmin(admin.ModelAdmin):
79+
"""
80+
Organisation admin representation
81+
"""
82+
form = OrganisationAdminForm
83+
inlines = [OrganisationMembershipInline, OrganisationAuthorityInline]
84+
#exclude = ['created', 'owner', 'members', 'brands']
85+
86+
list_filter = ['id', 'name']
87+
search_fields = ['id', 'name']
88+
list_display = ['id', 'name', 'slug']
89+
prepopulated_fields = {'slug': ['name']}
90+
91+
def get_form(self, request, obj=None, **kwargs):
92+
"""
93+
Responsible for pre-populating form data & resolving the associated model form
94+
95+
Args:
96+
request (RequestContext): the request context of the form
97+
obj (dict|None): an Organisation model instance (optional; defaults to `None`)
98+
**kwargs (**kwargs): arbitrary form key-value pair data
99+
100+
Returns:
101+
(OrganisationModelForm) - the prepared ModelForm instance
102+
"""
103+
form = super(OrganisationAdmin, self).get_form(request, obj, **kwargs)
104+
105+
if obj is None:
106+
form.base_fields['slug'].initial = ''
107+
form.base_fields['created'].initial = timezone.now()
108+
else:
109+
form.base_fields['slug'].initial = obj.slug
110+
form.base_fields['created'].initial = obj.created
111+
112+
form.base_fields['slug'].disabled = True
113+
form.base_fields['slug'].help_text = 'This field is not editable'
114+
form.base_fields['created'].disabled = True
115+
form.base_fields['created'].help_text = 'This field is not editable'
116+
117+
return form
75118

76119
@admin.register(Template)
77120
class TemplateAdmin(admin.ModelAdmin):

CodeListLibrary_project/clinicalcode/api/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ def get_schema(self, request=None, public=False):
139139
url(r'^data-sources/$',
140140
DataSource.get_datasources,
141141
name='data_sources'),
142+
url(r'^data-sources/(?P<datasource_id>[\d-]+)/export/$',
143+
DataSource.get_datasource_internal_detail,
144+
name='data_source_by_internal_id'),
142145
url(r'^data-sources/(?P<datasource_id>[\w-]+)/detail/$',
143146
DataSource.get_datasource_detail,
144147
name='data_source_by_id'),

CodeListLibrary_project/clinicalcode/api/views/Collection.py

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,46 @@
1+
from rest_framework import status
2+
from django.db.models import F, Q
3+
from rest_framework.response import Response
14
from rest_framework.decorators import (api_view, permission_classes)
25
from rest_framework.permissions import IsAuthenticatedOrReadOnly
3-
from rest_framework.response import Response
4-
from rest_framework import status
5-
from django.db.models import F
6+
from django.contrib.postgres.search import TrigramWordSimilarity
67

78
from ...models import Tag, GenericEntity
8-
from ...entity_utils import api_utils
9-
from ...entity_utils import constants
9+
from ...entity_utils import constants, gen_utils, api_utils
1010

1111
@api_view(['GET'])
1212
@permission_classes([IsAuthenticatedOrReadOnly])
1313
def get_collections(request):
1414
"""
15-
Get all collections
15+
Get all Collections
16+
17+
Available parameters:
18+
19+
| Param | Type | Default | Desc |
20+
|---------------|-----------------|---------|---------------------------------------------------------------|
21+
| search | `str` | `NULL` | Full-text search across _name_ field |
22+
| id | `int/list[int]` | `NULL` | Match by a single `int` _id_ field, or match by array overlap |
1623
"""
17-
collections = Tag.objects.filter(
18-
tag_type=constants.TAG_TYPE.COLLECTION.value
19-
) \
20-
.order_by('id')
21-
22-
result = collections.annotate(
23-
name=F('description')
24-
) \
25-
.values('id', 'name')
24+
search = request.query_params.get('search', '')
25+
26+
collections = Tag.get_brand_records_by_request(request, params={ 'tag_type': 2 })
27+
if collections is not None:
28+
if not gen_utils.is_empty_string(search) and len(search.strip()) > 1:
29+
collections = collections.annotate(
30+
similarity=TrigramWordSimilarity(search, 'description')
31+
) \
32+
.filter(Q(similarity__gte=0.7)) \
33+
.order_by('-similarity')
34+
else:
35+
collections = collections.order_by('id')
36+
37+
collections = collections.annotate(
38+
name=F('description')
39+
) \
40+
.values('id', 'name')
2641

2742
return Response(
28-
data=list(result),
43+
data=collections.values('id', 'name'),
2944
status=status.HTTP_200_OK
3045
)
3146

@@ -36,8 +51,11 @@ def get_collection_detail(request, collection_id):
3651
Get detail of specified collection by collection_id, including associated
3752
published entities
3853
"""
39-
collection = Tag.objects.filter(id=collection_id)
40-
if not collection.exists():
54+
collection = Tag.get_brand_assoc_queryset(request.BRAND_OBJECT, 'collections')
55+
if collection is not None:
56+
collection = collection.filter(id=collection_id)
57+
58+
if not collection or not collection.exists():
4159
return Response(
4260
data={
4361
'message': 'Collection with id does not exist'

CodeListLibrary_project/clinicalcode/api/views/Concept.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def get_concepts(request):
119119
query_clauses.append(psycopg2.sql.SQL('''(
120120
setweight(to_tsvector('pg_catalog.english', coalesce(historical.name,'')), 'A') ||
121121
setweight(to_tsvector('pg_catalog.english', coalesce(historical.description,'')), 'B')
122-
) @@ to_tsquery('pg_catalog.english', replace(websearch_to_tsquery('pg_catalog.english', %(search_query)s)::text || ':*', '<->', '|'))
122+
) @@ to_tsquery('pg_catalog.english', replace(to_tsquery('pg_catalog.english', concat(regexp_replace(trim(%(search_query)s), '\W+', ':* & ', 'gm'), ':*'))::text, '<->', '|'))
123123
'''))
124124

125125
# Resolve pagination behaviour
@@ -422,7 +422,7 @@ def get_concept_detail(request, concept_id, version_id=None, export_codes=False,
422422
if not user_can_access:
423423
return Response(
424424
data={
425-
'message': 'Concept version must be published or you must have permission to access it'
425+
'message': 'Entity version must be published or you must have permission to access it'
426426
},
427427
content_type='json',
428428
status=status.HTTP_401_UNAUTHORIZED

CodeListLibrary_project/clinicalcode/api/views/DataSource.py

Lines changed: 134 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,159 @@
1-
from rest_framework.decorators import (api_view, permission_classes)
2-
from rest_framework.permissions import IsAuthenticatedOrReadOnly
3-
from rest_framework.response import Response
41
from rest_framework import status
5-
from django.db.models import Subquery, OuterRef
2+
from django.db.models import Q, Subquery, OuterRef
3+
from rest_framework.response import Response
4+
from rest_framework.decorators import api_view, permission_classes
5+
from rest_framework.permissions import IsAuthenticatedOrReadOnly
6+
from django.contrib.postgres.search import TrigramWordSimilarity
67

78
from ...models import DataSource, Template, GenericEntity
8-
from ...entity_utils import api_utils
9-
from ...entity_utils import gen_utils
10-
from ...entity_utils import constants
9+
from ...entity_utils import api_utils, gen_utils, constants
1110

1211
@api_view(['GET'])
1312
@permission_classes([IsAuthenticatedOrReadOnly])
1413
def get_datasources(request):
1514
"""
16-
Get all datasources
15+
Get all DataSources
16+
17+
Available parameters:
18+
19+
| Param | Type | Default | Desc |
20+
|---------------|-----------------|---------|---------------------------------------------------------------|
21+
| search | `str` | `NULL` | Full-text search across _name_ and _description_ fields |
22+
| id | `int/list[int]` | `NULL` | Match by a single `int` _id_ field, or match by array overlap |
23+
| name | `str` | `NULL` | Case insensitive direct match of _name_ field |
24+
| uid | `str/uuid` | `NULL` | Case insensitive direct match of _uid_ field |
25+
| datasource_id | `int` | `NULL` | Match by exact _datasource_id_ |
26+
| url | `str` | `NULL` | Case insensitive direct match of _url_ field |
27+
| source | `str` | `NULL` | Case insensitive direct match of _source_ field |
1728
"""
18-
datasources = DataSource.objects.all().order_by('id')
19-
datasources = list(datasources.values('id', 'name', 'url', 'uid', 'source'))
29+
params = gen_utils.parse_model_field_query(DataSource, request, ignored_fields=['description'])
30+
if params is not None:
31+
datasources = DataSource.objects.filter(**params)
32+
else:
33+
datasources = DataSource.objects.all()
34+
35+
search = request.query_params.get('search')
36+
if not gen_utils.is_empty_string(search) and len(search.strip()) > 3:
37+
datasources = datasources.annotate(
38+
similarity=(
39+
TrigramWordSimilarity(search, 'name') + \
40+
TrigramWordSimilarity(search, 'description')
41+
)
42+
) \
43+
.filter(Q(similarity__gte=0.7)) \
44+
.order_by('-similarity')
45+
else:
46+
datasources = datasources.order_by('id')
47+
2048
return Response(
21-
data=datasources,
49+
data=datasources.values('id', 'name', 'description', 'url', 'uid', 'datasource_id', 'source'),
50+
status=status.HTTP_200_OK
51+
)
52+
53+
@api_view(['GET'])
54+
@permission_classes([IsAuthenticatedOrReadOnly])
55+
def get_datasource_internal_detail(request, datasource_id):
56+
"""
57+
Get detail of specified datasource by by its internal Id
58+
"""
59+
query = None
60+
if gen_utils.parse_int(datasource_id, default=None) is not None:
61+
query = { 'id': int(datasource_id) }
62+
63+
if not query:
64+
return Response(
65+
data={
66+
'message': 'Invalid id, expected int-like value'
67+
},
68+
content_type='json',
69+
status=status.HTTP_400_BAD_REQUEST
70+
)
71+
72+
datasource = DataSource.objects.filter(**query)
73+
if not datasource.exists():
74+
return Response(
75+
data={
76+
'message': 'Datasource with this internal Id does not exist'
77+
},
78+
content_type='json',
79+
status=status.HTTP_404_NOT_FOUND
80+
)
81+
82+
datasource = datasource.first()
83+
84+
# Get all templates and their versions where data_sources exist
85+
templates = Template.history.filter(
86+
definition__fields__has_key='data_sources'
87+
) \
88+
.annotate(
89+
was_deleted=Subquery(
90+
Template.history.filter(
91+
id=OuterRef('id'),
92+
history_date__gte=OuterRef('history_date'),
93+
history_type='-'
94+
)
95+
.order_by('id', '-history_id')
96+
.distinct('id')
97+
.values('id')
98+
)
99+
) \
100+
.exclude(was_deleted__isnull=False) \
101+
.order_by('id', '-template_version', '-history_id') \
102+
.distinct('id', 'template_version')
103+
104+
template_ids = list(templates.values_list('id', flat=True))
105+
template_versions = list(templates.values_list('template_version', flat=True))
106+
107+
# Get all published entities with this datasource
108+
entities = GenericEntity.history.filter(
109+
template_id__in=template_ids,
110+
template_version__in=template_versions,
111+
publish_status=constants.APPROVAL_STATUS.APPROVED.value
112+
) \
113+
.extra(where=[f"""
114+
exists(
115+
select 1
116+
from jsonb_array_elements(
117+
case jsonb_typeof(template_data->'data_sources') when 'array'
118+
then template_data->'data_sources'
119+
else '[]'
120+
end
121+
) as val
122+
where val in ('{datasource.id}')
123+
)"""
124+
]) \
125+
.order_by('id', '-history_id') \
126+
.distinct('id')
127+
128+
# Format results
129+
entities = api_utils.annotate_linked_entities(entities)
130+
131+
result = {
132+
'id': datasource.id,
133+
'name': datasource.name,
134+
'url': datasource.url,
135+
'uid': datasource.uid,
136+
'description': datasource.description,
137+
'source': datasource.source,
138+
'phenotypes': list(entities)
139+
}
140+
141+
return Response(
142+
data=result,
22143
status=status.HTTP_200_OK
23144
)
24145

25146
@api_view(['GET'])
26147
@permission_classes([IsAuthenticatedOrReadOnly])
27148
def get_datasource_detail(request, datasource_id):
28149
"""
29-
Get detail of specified datasource by datasource_id (id or HDRUK UUID for
30-
linkage between applications), including associated published entities
150+
Get detail of specified datasource by `datasource_id`, _i.e._ the HDRUK DataSource `pid` or its `UUID` for linkage between applications, including associated published entities.
31151
"""
32152
query = None
33153
if gen_utils.is_valid_uuid(datasource_id):
34154
query = { 'uid': datasource_id }
35155
elif gen_utils.parse_int(datasource_id, default=None) is not None:
36-
query = { 'id': int(datasource_id) }
156+
query = { 'datasource_id': int(datasource_id) }
37157

38158
if not query:
39159
return Response(

0 commit comments

Comments
 (0)