From b632406a22a3b4556bea89e366b4576e9a60726d Mon Sep 17 00:00:00 2001 From: Jon Clucas Date: Mon, 2 Dec 2019 13:34:11 -0500 Subject: [PATCH 1/6] :construction: --- CHANGELOG.rst | 4 ++ girderformindlogger/api/v1/applet.py | 61 +++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e322b0eae..c3c4c87e3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,10 @@ Changes ------- Unreleased ========== +2019-11-26: v0.7.1 +^^^^^^^^^^^^^^^^^^ +* :sparkles: Data access for reviewers + 2019-11-25: v0.6.6 ^^^^^^^^^^^^^^^^^^ * :ambulance: Fix `POST response/applet/activity` route diff --git a/girderformindlogger/api/v1/applet.py b/girderformindlogger/api/v1/applet.py index e4d52917c..dbb328ac2 100644 --- a/girderformindlogger/api/v1/applet.py +++ b/girderformindlogger/api/v1/applet.py @@ -49,6 +49,7 @@ def __init__(self): self.resourceName = 'applet' self._model = AppletModel() self.route('GET', (':id',), self.getApplet) + self.route('GET', (':id', 'data'), self.getAppletData) self.route('GET', (':id', 'groups'), self.getAppletGroups) self.route('POST', (), self.createApplet) self.route('PUT', (':id', 'informant'), self.updateInformant) @@ -193,6 +194,64 @@ def createApplet(self, protocolUrl=None, name=None, informant=None): ) return(applet) + @access.user(scope=TokenScope.DATA_WRITE) + @autoDescribeRoute( + Description('Create an applet.') + .param( + 'protocolUrl', + 'URL of Activity Set from which to create applet', + required=False + ) + .param( + 'name', + 'Name to give the applet. The Protocol\'s name will be used if ' + 'this parameter is not provided.', + required=False + ) + .param( + 'informant', + ' '.join([ + 'Relationship from informant to individual of interest.', + 'Currently handled informant relationships are', + str([r for r in DEFINED_INFORMANTS.keys()]) + ]), + required=False + ) + .errorResponse('Write access was denied for this applet.', 403) + ) + def getAppletData(self, protocolUrl=None, name=None, informant=None): + thisUser = self.getCurrentUser() + # get an activity set from a URL + protocol = ProtocolModel().getFromUrl( + protocolUrl, + 'protocol', + thisUser, + refreshCache=False + )[0] + protocol = protocol.get('protocol', protocol) + # create an applet for it + applet=AppletModel().createApplet( + name=name if name is not None and len(name) else ProtocolModel( + ).preferredName( + protocol + ), + protocol={ + '_id': 'protocol/{}'.format(protocol.get('_id')), + 'url': protocol.get( + 'meta', + {} + ).get( + 'protocol', + {} + ).get('url', protocolUrl) + }, + user=thisUser, + constraints={ + 'informantRelationship': informant + } if informant is not None else None + ) + return(applet) + @access.user(scope=TokenScope.DATA_WRITE) @autoDescribeRoute( Description('(managers only) Update the informant of an applet.') @@ -347,7 +406,7 @@ def getAppletRoles(self, folder): ) .param( 'role', - 'Role to invite this user to. One of ' + str(USER_ROLE_KEYS), + 'Role to invite this user to. One of ' + str(set(USER_ROLE_KEYS)), default='user', required=False, strip=True From c0c34d135964f346e27c0764642780d10eab0457 Mon Sep 17 00:00:00 2001 From: Jon Clucas Date: Wed, 18 Dec 2019 15:46:01 -0500 Subject: [PATCH 2/6] :construction: --- girderformindlogger/api/v1/applet.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/girderformindlogger/api/v1/applet.py b/girderformindlogger/api/v1/applet.py index 709962918..05ae3fe1c 100644 --- a/girderformindlogger/api/v1/applet.py +++ b/girderformindlogger/api/v1/applet.py @@ -185,30 +185,15 @@ def createApplet(self, protocolUrl=None, name=None, informant=None): @access.user(scope=TokenScope.DATA_WRITE) @autoDescribeRoute( - Description('Create an applet.') - .param( - 'protocolUrl', - 'URL of Activity Set from which to create applet', - required=False - ) + Description('Get all data you are authorized to see for an applet.') .param( - 'name', - 'Name to give the applet. The Protocol\'s name will be used if ' - 'this parameter is not provided.', - required=False - ) - .param( - 'informant', - ' '.join([ - 'Relationship from informant to individual of interest.', - 'Currently handled informant relationships are', - str([r for r in DEFINED_INFORMANTS.keys()]) - ]), - required=False + 'id', + 'ID of the applet for which to fetch data', + required=True ) .errorResponse('Write access was denied for this applet.', 403) ) - def getAppletData(self, protocolUrl=None, name=None, informant=None): + def getAppletData(self, id): thisUser = self.getCurrentUser() # get an activity set from a URL protocol = ProtocolModel().getFromUrl( From 2dc9403592e8bbab44c4d5a7d713ceae8551ded5 Mon Sep 17 00:00:00 2001 From: Jon Clucas Date: Fri, 20 Dec 2019 12:14:59 -0500 Subject: [PATCH 3/6] :sparkles: Add route to download Resolves https://github.com/ChildMindInstitute/MindLogger-bug-reports/issues/51 on-behalf-of: @ChildMindInstitute --- girderformindlogger/api/v1/applet.py | 59 ++++++++++++--------------- girderformindlogger/models/ID_code.py | 13 ++++-- girderformindlogger/models/applet.py | 35 +++++++++++++++- 3 files changed, 71 insertions(+), 36 deletions(-) diff --git a/girderformindlogger/api/v1/applet.py b/girderformindlogger/api/v1/applet.py index 117f2f7e0..98135cdc2 100644 --- a/girderformindlogger/api/v1/applet.py +++ b/girderformindlogger/api/v1/applet.py @@ -23,7 +23,7 @@ import uuid import requests from ..describe import Description, autoDescribeRoute -from ..rest import Resource +from ..rest import Resource, rawResponse from bson.objectid import ObjectId from girderformindlogger.constants import AccessType, SortDir, TokenScope, \ DEFINED_INFORMANTS, REPROLIB_CANONICAL, SPECIAL_SUBJECTS, USER_ROLES @@ -196,40 +196,35 @@ def createApplet(self, protocolUrl=None, name=None, informant=None): 'ID of the applet for which to fetch data', required=True ) + .param( + 'format', + 'JSON or CSV', + required=False + ) .errorResponse('Write access was denied for this applet.', 403) ) - def getAppletData(self, id): + def getAppletData(self, id, format='json'): + import pandas as pd + from datetime import datetime + from ..rest import setContentDisposition, setRawResponse, setResponseHeader + + format = ('json' if format is None else format).lower() thisUser = self.getCurrentUser() - # get an activity set from a URL - protocol = ProtocolModel().getFromUrl( - protocolUrl, - 'protocol', - thisUser, - refreshCache=False - )[0] - protocol = protocol.get('protocol', protocol) - # create an applet for it - applet=AppletModel().createApplet( - name=name if name is not None and len(name) else ProtocolModel( - ).preferredName( - protocol - ), - protocol={ - '_id': 'protocol/{}'.format(protocol.get('_id')), - 'url': protocol.get( - 'meta', - {} - ).get( - 'protocol', - {} - ).get('url', protocolUrl) - }, - user=thisUser, - constraints={ - 'informantRelationship': informant - } if informant is not None else None - ) - return(applet) + data = AppletModel().getResponseData(id, thisUser) + + setContentDisposition("{}-{}.{}".format( + str(id), + datetime.now().isoformat(), + format + )) + if format=='csv': + setRawResponse() + setResponseHeader('Content-Type', 'text/{}'.format(format)) + csv = pd.DataFrame(data).to_csv(index=False) + return(csv) + setResponseHeader('Content-Type', 'application/{}'.format(format)) + return(data) + @access.user(scope=TokenScope.DATA_WRITE) @autoDescribeRoute( diff --git a/girderformindlogger/models/ID_code.py b/girderformindlogger/models/ID_code.py index 198a112ea..f1b6f8f57 100644 --- a/girderformindlogger/models/ID_code.py +++ b/girderformindlogger/models/ID_code.py @@ -104,12 +104,20 @@ def load(self, id, level=AccessType.ADMIN, user=None, objectId=True, return doc def findIdCodes(self, profileId): - return([ + from .profile import Profile + + idCodes = [ i['code'] for i in list(self.find({'profileId': {'$in': [ str(profileId), ObjectId(profileId) ]}})) if isinstance(i, dict) and 'code' in i - ]) + ] + + if not len(idCodes): + self.createIdCode(Profile().load(profileId, force=True)) + return(self.findIdCodes(profileId)) + + return(idCodes) def removeCode(self, profileId, code): from .profile import Profile @@ -165,7 +173,6 @@ def createIdCode(self, profile, idCode=None): raise e print(sys.exc_info()) - def findProfile(self, idCode): """ Find a list of profiles for a given ID code. diff --git a/girderformindlogger/models/applet.py b/girderformindlogger/models/applet.py index d1f85c71b..fae535a3d 100644 --- a/girderformindlogger/models/applet.py +++ b/girderformindlogger/models/applet.py @@ -219,7 +219,40 @@ def getResponseData(self, appletId, reviewer, filter={}): :type filter: dict :reutrns: TBD """ - pass + from .ID_code import IDCode + from .profile import Profile + from .response_folder import ResponseItem + from .user import User + from pymongo import DESCENDING + + if not self._hasRole(appletId, reviewer, 'reviewer'): + raise AccessException("You are not a reviewer for this applet.") + query = { + "baseParentType": "user", + "meta.applet.@id": ObjectId(appletId) + } + responses = list(ResponseItem().find( + query=query, + user=reviewer, + sort=[("created", DESCENDING)] + )) + respondants = { + str(response['baseParentId']): IDCode().findIdCodes( + Profile().createProfile( + appletId, + User().load(response['baseParentId'], force=True), + 'user' + )['_id'] + ) for response in responses if 'baseParentId' in response + } + return([ + { + "respondant": code, + **response.get('meta', {}) + } for response in responses for code in respondants[ + str(response['baseParentId']) + ] + ]) def updateRelationship(self, applet, relationship): """ From f7570e01efdb1080d8c704ccb163b3d1a0329f47 Mon Sep 17 00:00:00 2001 From: Jon Clucas Date: Fri, 20 Dec 2019 14:01:40 -0500 Subject: [PATCH 4/6] :pencil: :lipstick: Correct spelling --- girderformindlogger/models/applet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/girderformindlogger/models/applet.py b/girderformindlogger/models/applet.py index fae535a3d..8da03221c 100644 --- a/girderformindlogger/models/applet.py +++ b/girderformindlogger/models/applet.py @@ -236,7 +236,7 @@ def getResponseData(self, appletId, reviewer, filter={}): user=reviewer, sort=[("created", DESCENDING)] )) - respondants = { + respondents = { str(response['baseParentId']): IDCode().findIdCodes( Profile().createProfile( appletId, @@ -247,9 +247,9 @@ def getResponseData(self, appletId, reviewer, filter={}): } return([ { - "respondant": code, + "respondent": code, **response.get('meta', {}) - } for response in responses for code in respondants[ + } for response in responses for code in respondents[ str(response['baseParentId']) ] ]) From 65d75e4e905fd910e00b54532c70c8f6d7200fab Mon Sep 17 00:00:00 2001 From: Jon Clucas Date: Fri, 20 Dec 2019 16:50:40 -0500 Subject: [PATCH 5/6] :hammer: https://github.com/ChildMindInstitute/mindlogger-app-backend/pull/256#discussion_r360572268 on-behalf-of: @ChildMindInstitute --- girderformindlogger/utility/jsonld_expander.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/girderformindlogger/utility/jsonld_expander.py b/girderformindlogger/utility/jsonld_expander.py index 77b1a15e3..240e1508a 100644 --- a/girderformindlogger/utility/jsonld_expander.py +++ b/girderformindlogger/utility/jsonld_expander.py @@ -315,8 +315,8 @@ def delanguageTag(obj): def expandOneLevel(obj): - if obj is None: - return(obj) + if not obj: + return(None) try: newObj = jsonld.expand(obj) except jsonld.JsonLdError as e: # 👮 Catch illegal JSON-LD From 8245e64beb875e2db7477e8c5767a53824cf9f41 Mon Sep 17 00:00:00 2001 From: Jon Clucas Date: Mon, 6 Jan 2020 14:34:03 -0500 Subject: [PATCH 6/6] :green_heart: :rewind: Revert 65d75e4 Catching falsy objects that are not Nones was causing excessive recursion. on-behalf-of: @ChildMindInstitute --- girderformindlogger/utility/jsonld_expander.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/girderformindlogger/utility/jsonld_expander.py b/girderformindlogger/utility/jsonld_expander.py index 240e1508a..ed336be73 100644 --- a/girderformindlogger/utility/jsonld_expander.py +++ b/girderformindlogger/utility/jsonld_expander.py @@ -315,8 +315,9 @@ def delanguageTag(obj): def expandOneLevel(obj): - if not obj: - return(None) + if obj is None: + # We only want to catch `None`s here, not other falsy objects + return(obj) try: newObj = jsonld.expand(obj) except jsonld.JsonLdError as e: # 👮 Catch illegal JSON-LD