Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Minors to Change age_range on Source #554

Merged
merged 16 commits into from
Dec 12, 2023
55 changes: 37 additions & 18 deletions microsetta_private_api/api/_consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,37 +62,56 @@ def check_consent_signature(account_id, source_id, consent_type, token_info):
def sign_consent_doc(account_id, source_id, consent_type, body, token_info):
_validate_account_access(token_info, account_id)

human_consent_age_groups = ["0-6", "7-12", "13-17", "18-plus"]
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved

with Transaction() as t:
# Sources with an age_range of "legacy" will select an age range
# the first time they sign a new consent document. We need to
# catch legacy sources as they come in and update their age.
# Sources are now permitted to update their age range, but only if it
# moves the source to an older age group. For this purpose, "legacy"
# is treated as younger than "0-6", as they're choosing an age group
# for the first time.
source_repo = SourceRepo(t)
source = source_repo.get_source(account_id, source_id)
if source is None:
return jsonify(code=404, message=SRC_NOT_FOUND_MSG), 404

if source.source_data.age_range == "legacy":
update_success = source_repo.update_legacy_source_age_range(
if source.source_data.age_range != body['age_range']:
# Let's make sure it's a valid change. First, grab the index of
# their current age range.
try:
cur_age_index = human_consent_age_groups.index(
source.source_data.age_range
)
except ValueError:
# Catch any sources that have a blank, "legacy", or faulty
# age_range
cur_age_index = -1

# Next, make sure their new age range is valid
try:
new_age_index = human_consent_age_groups.index(
body['age_range']
)
except ValueError:
# Shouldn't reach this point, but if we do, reject it
return jsonify(
code=403, message="Invalid age_range update"
), 403

# Finally, make sure the new age_range isn't younger than the
# current age_range.
if new_age_index < cur_age_index:
return jsonify(
code=403, message="Invalid age_range update"
), 403

update_success = source_repo.update_source_age_range(
source_id, body['age_range']
)
if not update_success:
return jsonify(
code=403, message="Invalid age_range update"
), 403

# NB For the time being, we need to block any pre-overhaul under-18
# profiles from re-consenting. For API purposes, the safest way to
# check whether it's a pre-overhaul or post-overhaul source is to look
# at the creation_time on the source. Anything pre-overhaul is
# prevented from signing a new consent document.
if source.source_data.age_range not in ["legacy", "18-plus"] and\
not source_repo.check_source_post_overhaul(
account_id, source_id
):
return jsonify(
code=403, message="Minors may not sign new consent documents"
), 403

# Now back to the normal flow of signing a consent document
consent_repo = ConsentRepo(t)
sign_id = str(uuid.uuid4())
Expand Down
2 changes: 2 additions & 0 deletions microsetta_private_api/api/microsetta_private_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ paths:
$ref: '#/components/schemas/consent_content'
assent_content:
$ref: '#/components/schemas/assent_content'
consent_type:
$ref: '#/components/schemas/consent_type'
'401':
$ref: '#/components/responses/401Unauthorized'
'403':
Expand Down
149 changes: 137 additions & 12 deletions microsetta_private_api/api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@
'age_range': "18-plus"
},
}
DUMMY_HUMAN_SOURCE_CHILD = {
'source_name': 'Bo',
'source_type': 'human',
'consent': {
'age_range': "7-12"
},
}
DUMMY_CONSENT_DATE = datetime.datetime.strptime('Jun 1 2005', '%b %d %Y')

PRIMARY_SURVEY_TEMPLATE_ID = 1 # primary survey
Expand Down Expand Up @@ -1739,11 +1746,13 @@ def sign_data_consent(self):

self.assertTrue(consent_status["result"])

CONSENT_ID = "b8245ca9-e5ba-4f8f-a84a-887c0d6a2233"

consent_data = copy.deepcopy(DUMMY_HUMAN_SOURCE)
consent_data.update("consent_type", ADULT_DATA_CONSENT)
consent_data.update("consent_id", CONSENT_ID)
consent_id = "b8245ca9-e5ba-4f8f-a84a-887c0d6a2233"
consent_data = {
"age_range": "18-plus",
"participant_name": "Bo",
"consent_type": ADULT_DATA_CONSENT,
"consent_id": consent_id
}

response = self.client.post(
'/api/accounts/%s/sources/%s/consent/%s' %
Expand All @@ -1757,30 +1766,146 @@ def sign_data_consent(self):
def sign_biospecimen_consent(self):
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved
"""Checks biospecimen consent for a source and sings the consent"""

dummy_acct_id, source_resp = create_dummy_source(
dummy_acct_id, dummy_source_resp = create_dummy_source(
"Bo", Source.SOURCE_TYPE_HUMAN, DUMMY_HUMAN_SOURCE,
create_dummy_1=True)

consent_status = self.client.get(
'/api/accounts/%s/sources/%s/consent/%s' %
(dummy_acct_id, source_resp["source_id"], BIOSPECIMEN_CONSENT),
(dummy_acct_id, dummy_source_resp["source_id"],
BIOSPECIMEN_CONSENT),
headers=self.dummy_auth)

self.assertTrue(consent_status["result"])

CONSENT_ID_BIO = "6b1595a5-4003-4d0f-aa91-56947eaf2901"
consent_id = "b8245ca9-e5ba-4f8f-a84a-887c0d6a2233"
consent_data = {
"age_range": "18-plus",
"participant_name": "Bo",
"consent_type": ADULT_BIOSPECIMEN_CONSENT,
"consent_id": consent_id
}

response = self.client.post(
'/api/accounts/%s/sources/%s/consent/%s' %
(dummy_acct_id, dummy_source_resp["source_id"],
BIOSPECIMEN_CONSENT),
content_type='application/json',
data=json.dumps(consent_data),
headers=self.dummy_auth)

self.assertEquals(201, response.status_code)

consent_data = copy.deepcopy(DUMMY_HUMAN_SOURCE)
consent_data.update("consent_type", ADULT_BIOSPECIMEN_CONSENT)
consent_data.update("consent_id", CONSENT_ID_BIO)
def sign_data_consent_new_age_invalid(self):
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved
"""In this test, we'll try to re-consent as an invalid age range"""

dummy_acct_id, dummy_source_resp = create_dummy_source(
"Bo", Source.SOURCE_TYPE_HUMAN, DUMMY_HUMAN_SOURCE,
create_dummy_1=True)

consent_status = self.client.get(
'/api/accounts/%s/sources/%s/consent/%s' %
(dummy_acct_id, dummy_source_resp["source_id"], DATA_CONSENT),
headers=self.dummy_auth)

self.assertTrue(consent_status["result"])

consent_id = "b8245ca9-e5ba-4f8f-a84a-887c0d6a2233"
consent_data = {
"age_range": "18-plus",
"participant_name": "Bo",
"consent_type": ADULT_DATA_CONSENT,
"consent_id": consent_id
}

response = self.client.post(
'/api/accounts/%s/sources/%s/consent/%s' %
(dummy_acct_id, source_resp["source_id"], BIOSPECIMEN_CONSENT),
(dummy_acct_id, dummy_source_resp["source_id"], DATA_CONSENT),
content_type='application/json',
data=json.dumps(consent_data),
headers=self.dummy_auth)

self.assertEquals(201, response.status_code)

# Now, try to sign another consent as a child
consent_data = {
"age_range": "7-12",
"assent_id": "27d6bd39-07b0-44d8-8c2e-05d80eb1eb56",
"consent_child": "Yes",
"participant_name": "Bo",
"consent_witness": "Yes",
"assent_obtainer": "Assent",
"consent_id": "b0d6be28-663b-4be8-9f54-f53f4cbf9a1f",
"consent_type": "parent_data",
"consent_parent": "Yes",
"parent_1_name": 'Parent'
}

response = self.client.post(
'/api/accounts/%s/sources/%s/consent/%s' %
(dummy_acct_id, dummy_source_resp["source_id"], DATA_CONSENT),
content_type='application/json',
data=json.dumps(consent_data),
headers=self.dummy_auth)

# And assert that it fails
self.assertEquals(403, response.status_code)

def sign_data_consent_new_age_valid(self):
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved
"""
In this test, we'll create a child source, then re-consent as an adult
"""

dummy_acct_id, dummy_source_resp = create_dummy_source(
"Bo", Source.SOURCE_TYPE_HUMAN, DUMMY_HUMAN_SOURCE_CHILD,
create_dummy_1=True)

consent_status = self.client.get(
'/api/accounts/%s/sources/%s/consent/%s' %
(dummy_acct_id, dummy_source_resp["source_id"], DATA_CONSENT),
headers=self.dummy_auth)

self.assertTrue(consent_status["result"])

consent_data = {
"age_range": "7-12",
"assent_id": "27d6bd39-07b0-44d8-8c2e-05d80eb1eb56",
"consent_child": "Yes",
"participant_name": "Bo",
"consent_witness": "Yes",
"assent_obtainer": "Assent",
"consent_id": "b0d6be28-663b-4be8-9f54-f53f4cbf9a1f",
"consent_type": "parent_data",
"consent_parent": "Yes",
"parent_1_name": 'Parent'
}

response = self.client.post(
'/api/accounts/%s/sources/%s/consent/%s' %
(dummy_acct_id, dummy_source_resp["source_id"], DATA_CONSENT),
content_type='application/json',
data=json.dumps(consent_data),
headers=self.dummy_auth)

self.assertEquals(201, response.status_code)

# Now, try to sign another consent as an adult
consent_id = "b8245ca9-e5ba-4f8f-a84a-887c0d6a2233"
consent_data = {
"age_range": "18-plus",
"participant_name": "Bo",
"consent_type": ADULT_DATA_CONSENT,
"consent_id": consent_id
}

response = self.client.post(
'/api/accounts/%s/sources/%s/consent/%s' %
(dummy_acct_id, dummy_source_resp["source_id"], DATA_CONSENT),
content_type='application/json',
data=json.dumps(consent_data),
headers=self.dummy_auth)

# And assert that it works
self.assertEquals(201, response.status_code)

def test_get_signed_consent(self):
Expand Down
10 changes: 7 additions & 3 deletions microsetta_private_api/model/consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def from_dict(input_dict, source_id, signature_id):
assent_id = input_dict.get("assent_id")
consent_content = input_dict.get("consent_content")
assent_content = input_dict.get("assent_content")
consent_type = input_dict.get("consent_type")

return ConsentSignature(
signature_id,
Expand All @@ -65,13 +66,14 @@ def from_dict(input_dict, source_id, signature_id):
assent_obtainer,
assent_id,
consent_content,
assent_content
assent_content,
consent_type
)

def __init__(self, signature_id, consent_id, source_id,
date_time, parent_1_name, parent_2_name,
deceased_parent, assent_obtainer, assent_id,
consent_content, assent_content):
consent_content, assent_content, consent_type):
self.signature_id = signature_id
self.consent_id = consent_id
self.source_id = source_id
Expand All @@ -83,6 +85,7 @@ def __init__(self, signature_id, consent_id, source_id,
self.assent_id = assent_id
self.consent_content = consent_content
self.assent_content = assent_content
self.consent_type = consent_type

def to_api(self):
return {'signature_id': self.signature_id,
Expand All @@ -95,5 +98,6 @@ def to_api(self):
'assent_obtainer': self.assent_obtainer,
'assent_id': self.assent_id,
'consent_content': self.consent_content,
'assent_content': self.assent_content
'assent_content': self.assent_content,
'consent_type': self.consent_type
}
34 changes: 24 additions & 10 deletions microsetta_private_api/repo/consent_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def _row_to_consent_signature(r):
r["assent_obtainer"],
r["assent_id"],
"",
"",
""
)

Expand Down Expand Up @@ -135,12 +136,16 @@ def is_consent_required(self, source_id, age_range, consent_type):
True if the user needs to reconsent, False otherwise
"""
if age_range == "18-plus":
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved
consent_join = "ca.consent_id"
consent_type = "adult_" + consent_type
elif age_range == "13-17":
consent_join = "ca.assent_id"
consent_type = "adolescent_" + consent_type
elif age_range == "7-12":
consent_join = "ca.assent_id"
consent_type = "child_" + consent_type
elif age_range == "0-6":
consent_join = "ca.consent_id"
consent_type = "parent_" + consent_type
else:
# Source is either "legacy" or lacks an age.
Expand All @@ -159,15 +164,20 @@ def is_consent_required(self, source_id, age_range, consent_type):
version = r['version']

# Now check if the source has agreed to that version of the given
# type of consent document
# type of consent document. For toddlers and adults, we check the
# consent_id column in ag.consent_audit, whereas kids and
# teens use the assent_id column.
sql = """SELECT ca.signature_id
FROM ag.consent_audit ca
INNER JOIN ag.consent_documents cd
ON {0} = cd.consent_id
WHERE cd.version = %s
AND cd.consent_type = %s
AND ca.source_id = %s"""
sql = sql.format(consent_join)
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved

cur.execute(
"SELECT ca.signature_id "
"FROM ag.consent_audit ca "
"INNER JOIN ag.consent_documents cd "
"ON ca.consent_id = cd.consent_id "
"WHERE cd.version = %s "
"AND cd.consent_type = %s "
"AND ca.source_id = %s",
sql,
(version, consent_type, source_id)
)
return cur.rowcount == 0
Expand Down Expand Up @@ -276,16 +286,20 @@ def get_latest_signed_consent(self, source_id, consent_type):
else:
consent_signature = _row_to_consent_signature(row)

survey_doc = self.get_consent_document(
consent_doc = self.get_consent_document(
consent_signature.consent_id
)
consent_signature.consent_content = survey_doc.consent_content
consent_signature.consent_content =\
consent_doc.consent_content

if consent_signature.assent_id is not None:
assent_doc = self.get_consent_document(
consent_signature.assent_id
)
consent_signature.assent_content =\
assent_doc.consent_content
consent_signature.consent_type = assent_doc.consent_type
else:
consent_signature.consent_type = consent_doc.consent_type

return consent_signature
Loading
Loading