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

Multivio #727

Merged
merged 8 commits into from
Jul 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 41 additions & 1 deletion amgut/handlers/add_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
from wtforms import (Form, SelectField, DateField, DateTimeField, TextField,
HiddenField, validators)
from tornado.web import authenticated
from tornado import escape
from future.utils import viewitems

from amgut.connections import ag_data
from amgut.lib.util import survey_vioscreen
from amgut.handlers.base_handlers import BaseHandler
from amgut import media_locale

Expand All @@ -21,6 +23,37 @@ class LogSample(Form):
notes = TextField('notes')


class AddHumanFFQHandler(BaseHandler):
@authenticated
def post(self):
self.redirect(media_locale['SITEBASE'] + '/authed/portal/')

def get(self):
ag_login_id = ag_data.get_user_for_kit(self.current_user)
barcode = self.get_secure_cookie('barcode')
participant_name = self.get_secure_cookie('participant_name')
self.clear_cookie('barcode')
self.clear_cookie('participant_name')

if barcode is None or participant_name is None:
self.set_status(404)
self.redirect(media_locale['SITEBASE'] + '/authed/portal/')
return
else:
barcode = escape.json_decode(barcode)
participant_name = escape.json_decode(participant_name)

new_survey_id = ag_data.get_new_survey_id()
ag_data.associate_barcode_to_survey_id(ag_login_id, participant_name,
barcode, new_survey_id)
ag_data.updateVioscreenStatus(new_survey_id, 0) # 0 -> not started
dat = survey_vioscreen(new_survey_id, None, None)

self.render('human_sample_specific_survey.html',
skid=self.current_user,
surveys=[dat])


class AddSample(BaseHandler):
_sample_sites = []
page_type = ''
Expand Down Expand Up @@ -74,7 +107,14 @@ def post(self):
env_sampled, sample_date,
sample_time, participant_name, notes)

self.redirect(media_locale['SITEBASE'] + '/authed/portal/')
if sample_site is None or self.page_type != 'add_sample_human':
self.redirect(media_locale['SITEBASE'] + '/authed/portal/')
else:
self.set_secure_cookie('participant_name',
escape.json_encode(participant_name))
self.set_secure_cookie('barcode', escape.json_encode(barcode))
url = media_locale['SITEBASE'] + '/authed/add_sample_human_ffq/'
self.redirect(url)

@authenticated
def get(self):
Expand Down
14 changes: 7 additions & 7 deletions amgut/handlers/animal_survey.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from urllib import urlencode
from json import dumps
import binascii
import os

from tornado.web import authenticated
from tornado.escape import url_escape
Expand Down Expand Up @@ -38,16 +36,18 @@ def post(self):
animal_survey_id = self.get_argument('survey_id', None)
sitebase = media_locale['SITEBASE']

form = self.animal_survey()
form.process(data=self.request.arguments)
data = {'questions': form.data}
participant_name = form['Pet_Information_127_0'].data[0]

if not animal_survey_id:
animal_survey_id = binascii.hexlify(os.urandom(8))
animal_survey_id = ag_data.get_new_survey_id()

new_survey = True
else:
new_survey = False

form = self.animal_survey()
form.process(data=self.request.arguments)
data = {'questions': form.data}
participant_name = form['Pet_Information_127_0'].data[0]
# If the participant already exists, stop them outright
if new_survey and \
ag_data.check_if_consent_exists(ag_login_id, participant_name):
Expand Down
4 changes: 1 addition & 3 deletions amgut/handlers/new_participant.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import binascii
import os
from json import dumps

from tornado.web import authenticated
Expand Down Expand Up @@ -57,7 +55,7 @@ def post(self):
self.redirect(url)
return

human_survey_id = binascii.hexlify(os.urandom(8))
human_survey_id = ag_data.get_new_survey_id()

consent = {'participant_name': participant_name,
'participant_email': participant_email,
Expand Down
4 changes: 1 addition & 3 deletions amgut/handlers/secondary_survey.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from urllib import urlencode
from tornado.escape import url_unescape
from json import dumps
import binascii
import os

from tornado.web import authenticated

Expand Down Expand Up @@ -54,7 +52,7 @@ def post(self):
sitebase = media_locale['SITEBASE']

if not survey_id:
survey_id = binascii.hexlify(os.urandom(8))
survey_id = ag_data.get_new_survey_id()

sec_survey = self.sec_surveys[survey_type]
survey_class = make_survey_class(sec_survey.groups[0],
Expand Down
80 changes: 78 additions & 2 deletions amgut/lib/data_access/ag_data_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import bcrypt
import numpy as np
import pandas as pd
import random
import string

from amgut.lib.data_access.sql_connection import TRN

Expand Down Expand Up @@ -540,10 +542,16 @@ def logParticipantSample(self, ag_login_id, barcode, sample_site,
participant_name, notes):
with TRN:
if sample_site is not None:
# Get survey id
# Get non timepoint specific survey IDs.
# As of this comment, a non timepoint specific survey is
# implicit, and currently limited to vioscreen FFQs
# We do not want to associate timepoint specific surveys
# with the wrong barcode
sql = """SELECT survey_id
FROM ag_login_surveys
WHERE ag_login_id = %s AND participant_name = %s"""
WHERE ag_login_id = %s
AND participant_name = %s
AND vioscreen_status is null"""

TRN.add(sql, (ag_login_id, participant_name))
survey_ids = TRN.execute_fetchindex()
Expand Down Expand Up @@ -634,6 +642,45 @@ def getHumanParticipants(self, ag_login_id):
TRN.add(sql, [ag_login_id, 1])
return TRN.execute_fetchflatten()

def associate_barcode_to_survey_id(self, ag_login_id, participant_name,
barcode, survey_id):
"""Associate a barcode to an existing survey ID

Parameters
----------
ag_login_id : str
A valid AG login ID
participant_name : str
The name of a participant associated with the login
barcode : str
A valid barcode associated with the login
survey_id : str
A valid survey ID
"""
with TRN:
# first let's sanity check things
sql = """SELECT ag_login_id, participant_name, barcode
FROM ag.ag_login_surveys
JOIN ag.source_barcodes_surveys USING(survey_id)
WHERE ag_login_id=%s
AND participant_name=%s
AND barcode=%s"""
TRN.add(sql, [ag_login_id, participant_name, barcode])
results = TRN.execute_fetchflatten()

if len(results) == 0:
raise ValueError("Unexpected name and ID relation")

sql = """INSERT INTO ag_login_surveys
(ag_login_id, survey_id, participant_name)
VALUES (%s, %s, %s)"""
TRN.add(sql, [ag_login_id, survey_id, participant_name])

sql = """INSERT INTO ag.source_barcodes_surveys
(survey_id, barcode)
VALUES (%s, %s)"""
TRN.add(sql, [survey_id, barcode])

def updateVioscreenStatus(self, survey_id, status):
with TRN:
sql = """UPDATE ag_login_surveys
Expand Down Expand Up @@ -1102,6 +1149,35 @@ def get_participants_surveys(self, ag_login_id, participant_name,
raise ValueError("No survey IDs found!")
return surveys

def get_new_survey_id(self):
"""Return a new unique survey ID

Notes
-----
This is *NOT* atomic. At the creation of this method, it is not
possible to store a survey ID without first storing consent. That
would require a fairly large structural change. This method replaces
the existing non-atomic logic, with logic that is much safer but not
perfect.

Returns
-------
str
A unique survey ID
"""
alpha = string.ascii_letters + string.digits
with TRN:
sql = """SELECT survey_id
FROM ag.ag_login_surveys"""
TRN.add(sql)
existing = {i[0] for i in TRN.execute()[0]}

new_id = ''.join([random.choice(alpha) for i in range(16)])
while new_id in existing:
new_id = ''.join([random.choice(alpha) for i in range(16)])

return new_id

def get_countries(self):
"""
Returns
Expand Down
92 changes: 92 additions & 0 deletions amgut/lib/data_access/test/test_ag_data_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,16 @@ def test_getConsentNotPresent(self):
with self.assertRaises(ValueError):
self.ag_data.getConsent("42")

def test_get_new_survey_id(self):
with TRN:
sql = """SELECT survey_id FROM ag.ag_login_surveys"""
TRN.add(sql)
results = {i[0] for i in TRN.execute()[0]}

for i in range(100):
new_id = self.ag_data.get_new_survey_id()
self.assertNotIn(new_id, results)

def test_logParticipantSample_badinfo(self):
# bad ag_login_id
with self.assertRaises(ValueError):
Expand All @@ -333,6 +343,88 @@ def test_logParticipantSample_badinfo(self):
'stool', None, datetime.date(2015, 9, 27),
datetime.time(15, 54), 'BADNAME', '')

@rollback
def test_associate_barcode_to_survey_id(self):
name = 'Name - öV2NA"+u+$'
id_ = '1835e434-b4a4-4f0d-a781-25ba54070c0b'
barcode = '000033139'

with TRN:
self.ag_data.associate_barcode_to_survey_id(id_, name, barcode,
'xyz')
self.ag_data.associate_barcode_to_survey_id(id_, name, barcode,
'yzx')
self.ag_data.associate_barcode_to_survey_id(id_, name, barcode,
'foo')
sql = """SELECT survey_id
FROM ag.source_barcodes_surveys
WHERE barcode = %s"""

TRN.add(sql, [barcode])
obs = set(TRN.execute_fetchflatten())
exp = {'xyz', 'yzx', 'foo'}
self.assertTrue(exp.issubset(obs))

with self.assertRaises(ValueError):
self.ag_data.associate_barcode_to_survey_id(id_, name + 'foo',
barcode, 'xyz')

with self.assertRaises(ValueError):
self.ag_data.associate_barcode_to_survey_id(id_, name,
'000004216', 'xyz')

@rollback
def test_logParticipantSample_avoid_vios(self):
participant_name = 'Name - öV2NA"+u+$'
ag_login_id = '1835e434-b4a4-4f0d-a781-25ba54070c0b'
barcode = '000033139'

focus_ffq = self.ag_data.get_new_survey_id()

# for example, the main survey which we then associate to the
# barcode in this method
main_questionnaire = 'not a vios'

other_ffq = self.ag_data.get_new_survey_id()

self.ag_data.associate_barcode_to_survey_id(ag_login_id,
participant_name,
barcode, focus_ffq)

with TRN:
sql = """INSERT INTO ag_login_surveys
(ag_login_id, survey_id, participant_name)
VALUES (%s, %s, %s)"""
TRN.add(sql, [ag_login_id, main_questionnaire, participant_name])
TRN.add(sql, [ag_login_id, other_ffq, participant_name])

self.ag_data.updateVioscreenStatus(focus_ffq, 0)
self.ag_data.updateVioscreenStatus(other_ffq, 0)

# sanity test prior to associating with main survey
with TRN:
sql = """SELECT survey_id FROM ag.source_barcodes_surveys
WHERE barcode = %s"""
TRN.add(sql, [barcode])
obs = set(TRN.execute_fetchflatten())
self.assertTrue(focus_ffq in obs)
self.assertFalse(main_questionnaire in obs)
self.assertFalse(other_ffq in obs)

self.ag_data.logParticipantSample(
ag_login_id, barcode, 'Stool', None, datetime.date(2015, 9, 27),
datetime.time(15, 54), participant_name, '')

# sanity test after associating to main survey
with TRN:
sql = """SELECT survey_id FROM ag.source_barcodes_surveys
WHERE barcode = %s"""
TRN.add(sql, [barcode])
obs = set(TRN.execute_fetchflatten())
self.assertTrue(focus_ffq in obs)
self.assertTrue(main_questionnaire in obs)
self.assertFalse(other_ffq in obs)

@rollback
def test_logParticipantSample_tomultiplesurveys(self):
ag_login_id = '5a10ea3e-9c7f-4ec3-9e96-3dc42e896668'
Expand Down
2 changes: 1 addition & 1 deletion amgut/lib/data_access/test/test_survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def insert_data(self):
c = ascii_letters + '1234567890'
notes_test = ''.join([choice(c) for i in range(40)])

survey_id = '817ff95701f4dd10'
survey_id = ag_data.get_new_survey_id()
survey = Survey(2)
consent = {
'login_id': 'eba20873-b7db-33cc-e040-8a80115d392c',
Expand Down
1 change: 1 addition & 0 deletions amgut/lib/locale_data/english_gut.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,7 @@
_HUMAN_SURVEY_COMPLETED = {
'AVAILABLE_SURVEYS': 'Below are a few additional surveys that you may be interested in completing. There is no requirement to take these surveys, and your decision does not affect your involvement in the project in any way.',
'COMPLETED_HEADER': 'Congratulations!',
'COMPLETED_SAMPLE_ASSIGNED_HEADER': 'Thank you for logging your sample!',
'COMPLETED_TEXT': 'You are now an enrolled participant in the %(PROJECT_TITLE)s! As a reminder, you still need to associate your sample(s) with the survey to complete the process. If your sample(s) are not associated with a survey, we will not be able to process them.' % media_locale,
'SURVEY_ASD': '<h3 style="text-align: center"><a href="%s" target="_blank">ASD-Cohort survey</a></h3><a href="http://www.anl.gov/contributors/jack-gilbert">Dr. Jack Gilbert</a> is exploring the relationship between gut dysbiosis and Autism Spectrum Disorders, and in conjunction with the {0}, we started an ASD-Cohort study. This additional survey contains questions specific to that cohort, but it is open to any participant to take if they so choose.'.format(AMGUT_CONFIG.project_name),
'SURVEY_VIOSCREEN': '<h3 style="text-align: center"><a href="%s">Dietary Survey</a></h3>The {0} and its sister projects are very interested in diet. If you\'d like to provide additional detail about your diet, please click above to take a detailed diet survey (known as an Food Frequency Questionnaire). This is a validated FFQ, and is the one used by the Mayo Clinic.'.format(AMGUT_CONFIG.project_name),
Expand Down
2 changes: 1 addition & 1 deletion amgut/lib/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def survey_surf(survey_id, consent_info, internal_surveys=[]):
return embedded_text % url


external_surveys = (survey_vioscreen, survey_fermented, survey_surf)
external_surveys = (survey_fermented, survey_surf)


def rollback(f):
Expand Down
20 changes: 20 additions & 0 deletions amgut/templates/human_sample_specific_survey.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends sitebase.html %}
{% block content %}
{% from amgut import text_locale, media_locale %}
{% set tl = text_locale['human_survey_completed.html'] %}
<h2>{% raw tl['COMPLETED_SAMPLE_ASSIGNED_HEADER'] %}</h2>
<div style="text-align: left; padding-left: 10px; padding-right: 10px">
<p>{% raw tl['AVAILABLE_SURVEYS'] %}</p>

{% for payload in surveys %}
<p>{% raw payload %}</p>
{% end %}
</div>
<form action='{% raw media_locale["SITEBASE"] %}/authed/human_survey_completed/' id="human_survey_completed" method="POST">
<input type="hidden" name="go_home" value="Done">
<input type="submit" value="Back to portal"></td>
</form>
<br>
<br/>
{% end %}