Skip to content

Commit

Permalink
✨ Relationships (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
shnizzedy authored Nov 22, 2019
2 parents 4fab8f4 + cae752c commit a75f787
Show file tree
Hide file tree
Showing 13 changed files with 541 additions and 65 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Changes
-------
Unreleased
==========
2019-11-21: v0.6.4
^^^^^^^^^^^^^^^^^^
* :sparkles: Informant-subject relationships

2019-11-20: v0.6.3
^^^^^^^^^^^^^^^^^^
* :hammer: Update invitation routes
Expand Down
5 changes: 3 additions & 2 deletions girderformindlogger/api/api_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import cherrypy

from . import describe
from .v1 import activity, api_key, applet, assetstore, file, \
collection, context, folder, group, invitation, item, protocol, resource, \
from .v1 import activity, api_key, applet, assetstore, collection, context, \
file, folder, group, invitation, item, protocol, relationship, resource, \
response, schedule, screen, system, token, user, notification


Expand Down Expand Up @@ -39,6 +39,7 @@ def _addV1ToNode(node):
node.v1.item = item.Item()
node.v1.notification = notification.Notification()
node.v1.protocol = protocol.Protocol()
node.v1.relationship = relationship.Relationship()
node.v1.resource = resource.Resource()
node.v1.response = response.ResponseItem()
node.v1.schedule = schedule.Schedule()
Expand Down
59 changes: 55 additions & 4 deletions girderformindlogger/api/v1/applet.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from ..rest import Resource
from bson.objectid import ObjectId
from girderformindlogger.constants import AccessType, SortDir, TokenScope, \
REPROLIB_CANONICAL, SPECIAL_SUBJECTS, USER_ROLES
DEFINED_INFORMANTS, REPROLIB_CANONICAL, SPECIAL_SUBJECTS, USER_ROLES
from girderformindlogger.api import access
from girderformindlogger.exceptions import AccessException, ValidationException
from girderformindlogger.models.activity import Activity as ActivityModel
Expand All @@ -51,6 +51,7 @@ def __init__(self):
self.route('GET', (':id',), self.getApplet)
self.route('GET', (':id', 'groups'), self.getAppletGroups)
self.route('POST', (), self.createApplet)
self.route('PUT', (':id', 'informant'), self.updateInformant)
self.route('PUT', (':id', 'assign'), self.assignGroup)
self.route('PUT', (':id', 'constraints'), self.setConstraints)
self.route('POST', (':id', 'invite'), self.invite)
Expand Down Expand Up @@ -148,16 +149,25 @@ def assignGroup(self, folder, group, role, subject):
'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 createApplet(self, protocolUrl=None, name=None, refreshCache=False):
def createApplet(self, protocolUrl=None, name=None, informant=None):
thisUser = self.getCurrentUser()
# get an activity set from a URL
protocol = ProtocolModel().getFromUrl(
protocolUrl,
'protocol',
thisUser,
refreshCache=refreshCache
refreshCache=False
)[0]
protocol = protocol.get('protocol', protocol)
# create an applet for it
Expand All @@ -176,10 +186,51 @@ def createApplet(self, protocolUrl=None, name=None, refreshCache=False):
{}
).get('url', protocolUrl)
},
user=thisUser
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.')
.modelParam(
'id',
model=AppletModel,
description='ID of the applet to update',
destName='applet',
force=True,
required=True
)
.param(
'informant',
' '.join([
'Relationship from informant to individual of interest.',
'Currently handled informant relationships are',
str([r for r in DEFINED_INFORMANTS.keys()])
]),
required=True
)
.errorResponse('Write access was denied for this applet.', 403)
)
def updateInformant(self, applet, informant):
user = self.getCurrentUser()
if not AppletModel().isManager(applet['_id'], user):
raise AccessException(
"Only managers can update informant relationship"
)
AppletModel().updateRelationship(applet, informant)
return(
jsonld_expander.formatLdObject(
applet,
'applet',
user,
refreshCache=False
)
)

@access.user(scope=TokenScope.DATA_WRITE)
@autoDescribeRoute(
Description('Deactivate an applet by ID.')
Expand Down
2 changes: 1 addition & 1 deletion girderformindlogger/api/v1/invitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@


class Invitation(Resource):
"""API Endpoint for schedules."""
"""API Endpoint for invitations."""

def __init__(self):
super(Invitation, self).__init__()
Expand Down
56 changes: 56 additions & 0 deletions girderformindlogger/api/v1/relationship.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

###############################################################################
# Copyright 2013 Kitware Inc.
#
# Licensed under the Apache License, Version 2.0 ( the "License" );
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###############################################################################

from ..describe import Description, autoDescribeRoute
from ..rest import Resource
from girderformindlogger.api import access
from girderformindlogger.constants import AccessType, DEFINED_INFORMANTS, \
DEFINED_RELATIONS, TokenScope
from girderformindlogger.exceptions import AccessException

class Relationship(Resource):
"""API Endpoint for relationships."""

def __init__(self):
super(Relationship, self).__init__()
self.resourceName = 'relationship'
self.route('GET', (), self.getDefinedRelations)
self.route('GET', ('informant',), self.getDefinedReports)

@access.public(scope=TokenScope.USER_INFO_READ)
@autoDescribeRoute(
Description('Get all currently-defined interpersonal relationships.')
.errorResponse()
)
def getDefinedRelations(self):
"""
Get all currently-defined relationships.
"""
return(DEFINED_RELATIONS)

@access.public(scope=TokenScope.USER_INFO_READ)
@autoDescribeRoute(
Description('Get all currently-defined informant relationships.')
.errorResponse()
)
def getDefinedReports(self):
"""
Get all currently-defined informant-subject reporting relationships.
"""
return(["{}-report".format(r) for r in DEFINED_INFORMANTS.keys()])
93 changes: 77 additions & 16 deletions girderformindlogger/api/v1/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(self):
self.route('GET', ('applets',), self.getOwnApplets)
self.route('GET', (':id', 'details'), self.getUserDetails)
self.route('GET', ('invites',), self.getGroupInvites)
self.route('PUT', (':id', 'knows'), self.setUserRelationship)
self.route('GET', ('details',), self.getUsersDetails)
self.route('POST', (), self.createUser)
self.route('PUT', (':id',), self.updateUser)
Expand Down Expand Up @@ -158,24 +159,84 @@ def find(self, text, limit, offset, sort):
def getUserByID(self, id):
from bson.objectid import ObjectId
user = self.getCurrentUser()
try:
p = ProfileModel().findOne({'_id': ObjectId(id)})
except:
p = None
if p is None:
appletList = AppletModel().getAppletsForUser(
'coordinator',
user,
active=False
return(ProfileModel().getProfile(id, user))

@access.public(scope=TokenScope.USER_INFO_READ)
@autoDescribeRoute(
Description('Add a relationship between users.')
.param(
'id',
'ID or ID code of user to add relationship to',
required=True
)
.param('rel', 'Relationship to add', required=True)
.param('otherId', 'ID or ID code of related individual.', required=True)
.param(
'otherName',
'Name to display for related individual',
required=True
)
.errorResponse('ID was invalid.')
.errorResponse('You do not have permission to see this user.', 403)
)
def setUserRelationship(self, id, rel, otherId, otherName):
from girderformindlogger.models.invitation import Invitation
from girderformindlogger.utility.jsonld_expander import \
inferRelationships, oidIffHex

user = self.getCurrentUser()
grammaticalSubject = ProfileModel().getProfile(id, user)
gsp = ProfileModel().load(
grammaticalSubject['_id'],
force=True
)
grammaticalSubject = Invitation().load(
grammaticalSubject['_id'],
force=True
) if gsp is None else gsp
print(grammaticalSubject)
if grammaticalSubject is None or not AppletModel().isCoordinator(
grammaticalSubject['appletId'], user
):
raise AccessException(
'You do not have permission to update this user.'
)
ps = [
ProfileModel().profileAsUser(
p,

appletId = grammaticalSubject['appletId']
grammaticalObject = ProfileModel().getSubjectProfile(
otherId,
otherName,
user
)
if grammaticalObject is None:
grammaticalObject = ProfileModel().getProfile(
ProfileModel().createPassiveProfile(
appletId,
otherId,
otherName,
user
) for p in IDCode().findProfile(id)
]
return(ps[0] if len(ps)==1 else ps)
return(ProfileModel().profileAsUser(p, user))
)['_id'],
grammaticalSubject
)
if 'schema:knows' in grammaticalSubject:
if rel in grammaticalSubject['schema:knows'] and grammaticalObject[
'_id'
] not in grammaticalSubject['schema:knows'][rel]:
grammaticalSubject['schema:knows'][rel].append(
grammaticalObject['_id']
)
else:
grammaticalSubject['schema:knows'][rel] = [
grammaticalObject['_id']
]
else:
grammaticalSubject['schema:knows'] = {
rel: [grammaticalObject['_id']]
}
ProfileModel().save(grammaticalSubject, validate=False)
inferRelationships(grammaticalSubject)
return(ProfileModel().getProfile(id, user))


@access.public(scope=TokenScope.USER_INFO_READ)
@autoDescribeRoute(
Expand Down
72 changes: 71 additions & 1 deletion girderformindlogger/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,75 @@
)
STATIC_ROOT_DIR = os.path.join(STATIC_PREFIX, 'web_client', 'static')

DEFINED_INFORMANTS = {
"parent": [
"schema:children",
"rel:parentOf"
],
"self": [
"schema:sameAs"
]
}

DEFINED_RELATIONS = {
"schema:knows": {
"rdf:type": ["owl:SymmetricProperty"],
"rdfs:comment": [
"The most generic bi-directional social/work relation."
]
},
"schema:sameAs": {
"rdf:type": ["owl:SymmetricProperty"],
"rdfs:comment": [
"URL of a reference Web page that unambiguously indicates the "
"item's identity. E.g. the URL of the item's Wikipedia page, "
"Wikidata entry, or official website."
]
},
"rel:parentOf": {
"owl:inverseOf": ["rel:childOf"],
"owl:equivalentProperty": ["schema:parent"],
"rdfs:subPropertyOf": ["schema:knows"],
"rdfs:comment": [
"A person who has given birth to or nurtured and raised this "
"person."
]
},
"schema:parent": {
"owl:inverseOf": ["schema:children"],
"owl:equivalentProperty": ["rel:childOf"],
"rdfs:subPropertyOf": ["schema:knows"],
"rdfs:comment": ["A parent of this person."]
},
"bio:father": {
"rdfs:subPropertyOf": ["schema:knows"],
"rdfs:comment": [
"The biological father of a person, also known as the genitor"
],
"owl:inverseOf": ["rel:parentOf"]
},
"bio:mother": {
"rdfs:subPropertyOf": ["schema:knows"],
"rdfs:comment": [
"The biological mother of a person, also known as the genetrix"
],
"owl:inverseOf": ["rel:parentOf"]
},
"rel:childOf": {
"owl:inverseOf": ["rel:parentOf"],
"owl:equivalentProperty": ["schema:children"],
"rdfs:comment": [
"A person who was given birth to or nurtured and raised by this "
"person."
]
},
"schema:children": {
"owl:inverseOf": ["schema:parent"],
"owl:equivalentProperty": ["rel:parentOf"],
"rdfs:comment": ["A child of the person."]
}
}

PREFERRED_NAMES = ["skos:prefLabel", "skos:altLabel", "name", "@id", "url"]

HIERARCHY = ['applet', 'protocol', 'activity', 'screen', 'item']
Expand Down Expand Up @@ -63,7 +132,8 @@

PROFILE_FIELDS = [
'_id',
'displayName'
'displayName',
'schema:knows'
]

def MODELS():
Expand Down
Loading

0 comments on commit a75f787

Please sign in to comment.