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

Explicit Consent Versioning #548

Merged
merged 4 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion microsetta_private_api/api/_consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ def check_consent_signature(account_id, source_id, consent_type, token_info):

with Transaction() as t:
consent_repo = ConsentRepo(t)
res = consent_repo.is_consent_required(source_id, consent_type)
source_repo = SourceRepo(t)
source = source_repo.get_source(account_id, source_id)
age_range = source.source_data.age_range
res = consent_repo.is_consent_required(
source_id, age_range, consent_type
)

return jsonify({"result": res}), 200

Expand Down
3 changes: 2 additions & 1 deletion microsetta_private_api/api/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
CONSENT_DOC = {"consent_type": "adult_data",
"locale": "en_US",
"consent": "Adult Data Consent",
"reconsent": 'true'
"reconsent": 'true',
"version": 9999
}


Expand Down
18 changes: 18 additions & 0 deletions microsetta_private_api/db/patches/0134.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- We're going to add explicit versioning to the consent documents.
-- For the time being, this will only be used for checking whether reconsent
-- is necessary, and we're going to assume that every locale and age range is
-- kept in sync at the same version. If that ever changes - e.g. en_US adults
-- are on a different version than es_MX adults - we will likely need to modify
-- a variety of both code and operating procedures.
ALTER TABLE ag.consent_documents ADD COLUMN version INTEGER;

-- We're arbitrarily going to consider the versions that were created for
-- relaunch as v1, since that's the first version for which we have a full
-- audit trail.
UPDATE ag.consent_documents SET version = 1;

-- Now that a value exists for all documents, we're going to add a NOT NULL
-- constraint and a unique constraint on the combination of consent_type +
-- locale + version
ALTER TABLE ag.consent_documents ALTER COLUMN version SET NOT NULL;
ALTER TABLE ag.consent_documents ADD CONSTRAINT idx_consent_type_locale_version UNIQUE (consent_type, locale, version);
19 changes: 11 additions & 8 deletions microsetta_private_api/model/consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,34 @@ def from_dict(input_dict, account_id, consent_id):
date_time = datetime.now()
consent = input_dict["consent"]
reconsent = input_dict["reconsent"]
version = input_dict["version"]
return ConsentDocument(
consent_id, consent_type, locale,
date_time, consent, account_id,
reconsent
reconsent, version
)

def __init__(self, consent_id, consent_type, locale,
date_time, consent_content,
account_id, reconsent):
account_id, reconsent, version):
self.consent_id = consent_id
self.consent_type = consent_type
self.locale = locale
self.date_time = date_time
self.consent_content = consent_content
self.account_id = account_id
self.reconsent = reconsent
self.version = version

def to_api(self):
result = {
"consent_id": self.consent_id,
"consent_type": self.consent_type,
"locale": self.locale,
"document": self.consent_content,
"reconsent_required": self.reconsent
}
"consent_id": self.consent_id,
"consent_type": self.consent_type,
"locale": self.locale,
"document": self.consent_content,
"reconsent_required": self.reconsent,
"version": self.version
}

return result

Expand Down
92 changes: 60 additions & 32 deletions microsetta_private_api/repo/consent_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def _consent_document_to_row(s):
s.date_time,
s.consent_content,
s.account_id,
s.reconsent)
s.reconsent,
s.version)
return row


Expand All @@ -25,7 +26,9 @@ def _row_to_consent_document(r):
r["date_time"],
r["consent_content"],
getattr(r, 'account_id', None),
r["reconsent_required"])
r["reconsent_required"],
r["version"]
)


def _consent_signature_to_row(s):
Expand Down Expand Up @@ -64,11 +67,12 @@ def __init__(self, transaction):
super().__init__(transaction)

doc_read_cols = "distinct on (consent_type) consent_id, consent_type, " \
"locale, date_time, consent_content, reconsent_required" \
"locale, date_time, consent_content, reconsent_required,"\
" version"

doc_write_cols = "consent_id, consent_type, " \
"locale, date_time, consent_content, " \
"account_id, reconsent_required"
"account_id, reconsent_required, version"

signature_read_cols = "signature_id, consent_type, " \
"consent_audit.date_time AS sign_date, "\
Expand All @@ -84,7 +88,7 @@ def create_doc(self, consent):
cur.execute("INSERT INTO ag.consent_documents (" +
self.doc_write_cols + ") "
"VALUES( %s, %s, %s, %s, %s, "
"%s, %s) ",
"%s, %s, %s) ",
_consent_document_to_row(consent))
return cur.rowcount == 1

Expand Down Expand Up @@ -113,36 +117,60 @@ def get_consent_document(self, consent_id):
else:
return _row_to_consent_document(r)

def is_consent_required(self, source_id, consent_type):
def is_consent_required(self, source_id, age_range, consent_type):
"""Determine whether a source needs to agree to a new consent document

Parameters
----------
source_id : uuid4
The id of the source whose consent needs to be verified
age_range : str
The source's age range
consent_type : str
data or biospecimen

Returns
-------
bool
True if the user needs to reconsent, False otherwise
"""
with self._transaction.dict_cursor() as cur:
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved
cur.execute("SELECT " + self.signature_read_cols + " FROM "
"ag.consent_audit INNER JOIN ag.consent_documents "
"USING (consent_id) "
"WHERE consent_audit.source_id = %s and "
"consent_documents.consent_type "
"LIKE %s ORDER BY sign_date DESC",
(source_id, ('%' + consent_type + '%')))
if age_range == "18-plus":
consent_type = "adult_" + consent_type
elif age_range == "13-17":
consent_type = "adolescent_" + consent_type
elif age_range == "7-12":
consent_type = "child_" + consent_type
elif age_range == "0-6":
consent_type = "parent_" + consent_type
else:
# Source is either "legacy" or lacks an age.
# Either way, make them reconsent so they're forced to choose
# an age range.
return True
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved

# Grab the maximum consent version that requires reconsent
cur.execute(
"SELECT MAX(version) AS version "
"FROM ag.consent_documents "
"WHERE reconsent_required = TRUE"
)
r = cur.fetchone()
if r is None:
return True
elif r['reconsent_required']:
consent_doc_type = r["consent_type"]
cur.execute("SELECT date_time FROM "
"ag.consent_documents WHERE consent_type = %s "
"ORDER BY date_time DESC LIMIT 1",
(consent_doc_type,))

s = cur.fetchone()
if s is None:
return True
else:
sign_date = r["sign_date"]
doc_date = s["date_time"]

return doc_date > sign_date
else:
return r["reconsent_required"]
version = r['version']

# Now check if the source has agreed to that version of the given
# type of consent document
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",
(version, consent_type, source_id)
)
return cur.rowcount == 0

def _is_valid_consent_sign(self, sign, doc):
res = True
Expand Down
Loading
Loading