Skip to content

Commit 61d7e1b

Browse files
Merge pull request #600 from biocore/csymons_skin_scoring_app_working
Skin Scoring App
2 parents 5287a90 + 2c97fd3 commit 61d7e1b

11 files changed

+774
-47
lines changed

microsetta_private_api/api/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
read_survey_template, read_survey_templates, read_answered_survey,
2626
read_answered_surveys, submit_answered_survey,
2727
read_answered_survey_associations, top_food_report,
28-
read_myfoodrepo_available_slots
28+
read_myfoodrepo_available_slots, get_skin_scoring_app_credentials,
29+
post_skin_scoring_app_credentials
2930
)
3031
from ._sample import (
3132
read_sample_association, associate_sample, read_sample_associations,
@@ -103,6 +104,8 @@
103104
'read_answered_survey_associations',
104105
'top_food_report',
105106
'read_myfoodrepo_available_slots',
107+
'get_skin_scoring_app_credentials',
108+
'post_skin_scoring_app_credentials',
106109
'read_sample_association',
107110
'associate_sample',
108111
'read_sample_associations',

microsetta_private_api/api/_survey.py

+106-19
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,40 @@ def read_survey_templates(account_id, source_id, language_tag, token_info):
3434
with Transaction() as t:
3535
source_repo = SourceRepo(t)
3636
source = source_repo.get_source(account_id, source_id)
37+
3738
if source is None:
3839
return jsonify(code=404, message="No source found"), 404
40+
3941
template_repo = SurveyTemplateRepo(t)
42+
4043
if source.source_type == Source.SOURCE_TYPE_HUMAN:
41-
return jsonify([template_repo.get_survey_template_link_info(x)
42-
for x in [
43-
SurveyTemplateRepo.VIOSCREEN_ID,
44-
SurveyTemplateRepo.POLYPHENOL_FFQ_ID,
45-
SurveyTemplateRepo.SPAIN_FFQ_ID,
46-
SurveyTemplateRepo.BASIC_INFO_ID,
47-
SurveyTemplateRepo.AT_HOME_ID,
48-
SurveyTemplateRepo.LIFESTYLE_ID,
49-
SurveyTemplateRepo.GUT_ID,
50-
SurveyTemplateRepo.GENERAL_HEALTH_ID,
51-
SurveyTemplateRepo.HEALTH_DIAG_ID,
52-
SurveyTemplateRepo.ALLERGIES_ID,
53-
SurveyTemplateRepo.DIET_ID,
54-
SurveyTemplateRepo.DETAILED_DIET_ID,
55-
SurveyTemplateRepo.OTHER_ID
56-
]]), 200
44+
template_ids = [
45+
SurveyTemplateRepo.VIOSCREEN_ID,
46+
SurveyTemplateRepo.POLYPHENOL_FFQ_ID,
47+
SurveyTemplateRepo.SPAIN_FFQ_ID,
48+
SurveyTemplateRepo.BASIC_INFO_ID,
49+
SurveyTemplateRepo.AT_HOME_ID,
50+
SurveyTemplateRepo.LIFESTYLE_ID,
51+
SurveyTemplateRepo.GUT_ID,
52+
SurveyTemplateRepo.GENERAL_HEALTH_ID,
53+
SurveyTemplateRepo.HEALTH_DIAG_ID,
54+
SurveyTemplateRepo.ALLERGIES_ID,
55+
SurveyTemplateRepo.DIET_ID,
56+
SurveyTemplateRepo.DETAILED_DIET_ID,
57+
SurveyTemplateRepo.OTHER_ID
58+
]
59+
if template_repo.check_display_skin_scoring_app(
60+
account_id, source_id
61+
):
62+
template_ids.append(SurveyTemplateRepo.SKIN_SCORING_APP_ID)
63+
5764
elif source.source_type == Source.SOURCE_TYPE_ANIMAL:
58-
return jsonify([template_repo.get_survey_template_link_info(x)
59-
for x in [2]]), 200
65+
template_ids = [2]
6066
else:
61-
return jsonify([]), 200
67+
template_ids = []
68+
69+
return jsonify([template_repo.get_survey_template_link_info(x)
70+
for x in template_ids]), 200
6271

6372

6473
def _remote_survey_url_vioscreen(transaction, account_id, source_id,
@@ -181,6 +190,23 @@ def _remote_survey_url_spain_ffq(transaction, account_id, source_id):
181190
return SERVER_CONFIG['spain_ffq_url']
182191

183192

193+
def _remote_survey_url_skin_scoring_app(transaction,
194+
account_id,
195+
source_id):
196+
st_repo = SurveyTemplateRepo(transaction)
197+
198+
# Confirm that the user has credentials allocated
199+
ssa_u, _ = st_repo.get_skin_scoring_app_credentials_if_exists(
200+
account_id,
201+
source_id
202+
)
203+
204+
if ssa_u is None:
205+
raise NotFound("Sorry, you were not allocated credentials")
206+
207+
return SERVER_CONFIG['skin_app_url']
208+
209+
184210
def read_survey_template(account_id, source_id, survey_template_id,
185211
language_tag, token_info, survey_redirect_url=None,
186212
vioscreen_ext_sample_id=None,
@@ -220,6 +246,11 @@ def read_survey_template(account_id, source_id, survey_template_id,
220246
url = _remote_survey_url_spain_ffq(t,
221247
account_id,
222248
source_id)
249+
elif survey_template_id == \
250+
SurveyTemplateRepo.SKIN_SCORING_APP_ID:
251+
url = _remote_survey_url_skin_scoring_app(t,
252+
account_id,
253+
source_id)
223254
else:
224255
raise ValueError(f"Cannot generate URL for survey "
225256
f"{survey_template_id}")
@@ -499,3 +530,59 @@ def read_myfoodrepo_available_slots():
499530
resp = jsonify(code=200, number_of_available_slots=available,
500531
total_number_of_slots=total)
501532
return resp, 200
533+
534+
535+
def get_skin_scoring_app_credentials(account_id, source_id, token_info):
536+
_validate_account_access(token_info, account_id)
537+
538+
with Transaction() as t:
539+
st_repo = SurveyTemplateRepo(t)
540+
ssa_u, ssa_s = st_repo.get_skin_scoring_app_credentials_if_exists(
541+
account_id, source_id
542+
)
543+
response_obj = {
544+
"app_username": ssa_u,
545+
"app_studycode": ssa_s
546+
}
547+
return jsonify(response_obj), 200
548+
549+
550+
def post_skin_scoring_app_credentials(account_id, source_id, token_info):
551+
_validate_account_access(token_info, account_id)
552+
553+
with Transaction() as t:
554+
st_repo = SurveyTemplateRepo(t)
555+
556+
# First, confirm that the source doesn't already have credentials
557+
ssa_u, _ = st_repo.get_skin_scoring_app_credentials_if_exists(
558+
account_id, source_id
559+
)
560+
561+
# This shouldn't happen, but if it does, return an error
562+
if ssa_u is not None:
563+
return jsonify(
564+
code=400,
565+
message="Credentials already exist"
566+
), 400
567+
568+
# Now, try to allocate credentials and create an entry in the skin
569+
# scoring app registry table
570+
ssa_u, ssa_s = st_repo.create_skin_scoring_app_entry(
571+
account_id, source_id
572+
)
573+
t.commit()
574+
575+
if ssa_u is None:
576+
# No credentials were available
577+
return jsonify(
578+
code=404,
579+
message="No credentials available"
580+
), 404
581+
else:
582+
# Credentials were successfully allocated
583+
return jsonify(
584+
{
585+
"app_username": ssa_u,
586+
"app_studycode": ssa_s
587+
}
588+
), 201

microsetta_private_api/api/microsetta_private_api.yaml

+58
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,64 @@ paths:
10241024
'404':
10251025
$ref: '#/components/responses/404NotFound'
10261026

1027+
'/accounts/{account_id}/sources/{source_id}/surveys/skin_scoring_app_credentials':
1028+
get:
1029+
operationId: microsetta_private_api.api.get_skin_scoring_app_credentials
1030+
tags:
1031+
- Surveys (By Source)
1032+
summary: Get skin scoring app credentials associated with source, if they exist
1033+
description: Get skin scoring app credentials associated with source, if they exist
1034+
parameters:
1035+
- $ref: '#/components/parameters/account_id'
1036+
- $ref: '#/components/parameters/source_id'
1037+
responses:
1038+
'200':
1039+
description: Credentials for skin scoring app
1040+
content:
1041+
application/json:
1042+
schema:
1043+
type: object
1044+
properties:
1045+
app_username:
1046+
type: string
1047+
nullable: true
1048+
app_studycode:
1049+
type: string
1050+
nullable: true
1051+
'401':
1052+
$ref: '#/components/responses/401Unauthorized'
1053+
'403':
1054+
$ref: '#/components/responses/403Forbidden'
1055+
post:
1056+
operationId: microsetta_private_api.api.post_skin_scoring_app_credentials
1057+
tags:
1058+
- Surveys (By Source)
1059+
summary: Create association between a set of skin scoring app credentials and a source
1060+
description: Create association between a set of skin scoring app credentials and a source
1061+
parameters:
1062+
- $ref: '#/components/parameters/account_id'
1063+
- $ref: '#/components/parameters/source_id'
1064+
responses:
1065+
'201':
1066+
description: Credentials for skin scoring app
1067+
content:
1068+
application/json:
1069+
schema:
1070+
type: object
1071+
properties:
1072+
app_username:
1073+
type: string
1074+
app_studycode:
1075+
type: string
1076+
'400':
1077+
description: 'Credentials already exist for source'
1078+
'401':
1079+
$ref: '#/components/responses/401Unauthorized'
1080+
'403':
1081+
$ref: '#/components/responses/403Forbidden'
1082+
'404':
1083+
$ref: '#/components/responses/404NotFound'
1084+
10271085
'/accounts/{account_id}/sources/{source_id}/samples':
10281086
get:
10291087
operationId: microsetta_private_api.api.read_sample_associations

microsetta_private_api/api/tests/test_api.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@
181181
'sample_projects': ['American Gut Project'],
182182
'account_id': None,
183183
'source_id': None,
184-
'sample_site': None}
184+
'sample_site': None,
185+
'sample_project_ids': [1]}
185186

186187
DUMMY_FILLED_SAMPLE_INFO = {
187188
'sample_barcode': BARCODE,
@@ -195,7 +196,8 @@
195196
'sample_projects': ['American Gut Project'],
196197
'account_id': 'foobar',
197198
'source_id': 'foobarbaz',
198-
'sample_site': 'Saliva'}
199+
'sample_site': 'Saliva',
200+
'sample_project_ids': [1]}
199201

200202
ACCT_ID_KEY = "account_id"
201203
ACCT_TYPE_KEY = "account_type"
@@ -594,7 +596,8 @@ def create_dummy_sample_objects(filled=False):
594596
info_dict['account_id'],
595597
None,
596598
info_dict["sample_projects"],
597-
None)
599+
None,
600+
sample_project_ids=info_dict["sample_project_ids"])
598601

599602
return sample_info, sample
600603
# endregion help methods
@@ -2244,6 +2247,10 @@ def test_associate_sample_to_source_success(self):
22442247
exp['account_id'] = ACCT_ID_1
22452248
exp['kit_id'] = None
22462249

2250+
# Remove the sample_project_ids element since we don't expect that
2251+
# to come out of the API
2252+
exp.pop("sample_project_ids")
2253+
22472254
self.assertEqual(get_resp_obj, [exp])
22482255

22492256
# TODO: We should also have tests of associating a sample to a source

microsetta_private_api/db/migration_support.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,35 @@ def migrate_133(TRN):
795795
print("No mapping: " + ffq_id + " - " + barcode)
796796
TRN.execute()
797797

798+
@staticmethod
799+
def migrate_144(TRN):
800+
# We need to load the credentials that the vendor provided in CSV form
801+
# Format is username, studycode and includes a header row
802+
skin_app_credentials_path = SERVER_CONFIG["skin_app_credentials_path"]
803+
if not os.path.exists(skin_app_credentials_path):
804+
print(
805+
"Credentials for app not found:" + skin_app_credentials_path
806+
)
807+
return
808+
809+
with open(skin_app_credentials_path) as csv_file:
810+
csv_contents = csv.reader(csv_file)
811+
header = True
812+
813+
for csv_row in csv_contents:
814+
if header:
815+
header = False
816+
continue
817+
app_username, app_studycode = csv_row
818+
819+
TRN.add(
820+
"INSERT INTO ag.skin_scoring_app_credentials "
821+
"(app_username, app_studycode) "
822+
"VALUES (%s, %s)",
823+
(app_username, app_studycode)
824+
)
825+
TRN.execute()
826+
798827
MIGRATION_LOOKUP = {
799828
"0048.sql": migrate_48.__func__,
800829
"0050.sql": migrate_50.__func__,
@@ -806,7 +835,8 @@ def migrate_133(TRN):
806835
# "0082.sql": migrate_82.__func__
807836
# ...
808837
"0096.sql": migrate_96.__func__,
809-
"0133.sql": migrate_133.__func__
838+
"0133.sql": migrate_133.__func__,
839+
"0144.sql": migrate_144.__func__
810840
}
811841

812842
@classmethod
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- The organization that hosts the skin-scoring app provides us with username
2+
-- and studycode pairings for participants to access the app. We need to store
3+
-- these pairings, as well as a flag for whether the pairing has been
4+
-- allocated to a participant. We explicitly store this flag to avoid reuse
5+
-- if sources (and their related survey databsase records) were to be deleted.
6+
CREATE TABLE ag.skin_scoring_app_credentials (
7+
app_username VARCHAR PRIMARY KEY,
8+
app_studycode VARCHAR NOT NULL,
9+
credentials_allocated BOOLEAN NOT NULL DEFAULT FALSE
10+
);
11+
12+
-- And we create a registry table, similar to all of the other external
13+
-- surveys we've hosted in the past, to link the username to the account and
14+
-- source that used it.
15+
CREATE TABLE ag.skin_scoring_app_registry (
16+
app_username VARCHAR PRIMARY KEY,
17+
account_id UUID NOT NULL,
18+
source_id UUID,
19+
deleted BOOLEAN NOT NULL DEFAULT false,
20+
creation_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
21+
22+
CONSTRAINT fk_skin_scoring_app_username FOREIGN KEY (app_username) REFERENCES ag.skin_scoring_app_credentials(app_username),
23+
CONSTRAINT fk_skin_scoring_app_registry_account FOREIGN KEY (account_id) REFERENCES ag.account(id),
24+
CONSTRAINT fk_skin_scoring_app_registry_source FOREIGN KEY (source_id) REFERENCES ag.source(id)
25+
);
26+
27+
CREATE INDEX skin_scoring_app_registry_source ON ag.skin_scoring_app_registry (account_id, source_id);

microsetta_private_api/model/sample.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class Sample(ModelBase):
66
def __init__(self, sample_id, datetime_collected, site, notes, barcode,
77
latest_scan_timestamp, source_id, account_id,
88
latest_sample_information_update, sample_projects,
9-
latest_scan_status, kit_id=None):
9+
latest_scan_status, kit_id=None, sample_project_ids=None):
1010
self.id = sample_id
1111
# NB: datetime_collected may be None if sample not yet used
1212
self.datetime_collected = datetime_collected
@@ -28,9 +28,14 @@ def __init__(self, sample_id, datetime_collected, site, notes, barcode,
2828
self.accession_urls = []
2929
self.kit_id = kit_id
3030

31+
self._sample_project_ids = sample_project_ids
32+
3133
def set_accession_urls(self, accession_urls):
3234
self.accession_urls = accession_urls
3335

36+
def get_project_ids(self):
37+
return self._sample_project_ids
38+
3439
@property
3540
def edit_locked(self):
3641
# If a sample has been scanned and is valid, it is locked.
@@ -47,7 +52,8 @@ def remove_locked(self):
4752
def from_db(cls, sample_id, date_collected, time_collected,
4853
site, notes, barcode, latest_scan_timestamp,
4954
latest_sample_information_update, source_id,
50-
account_id, sample_projects, latest_scan_status):
55+
account_id, sample_projects, latest_scan_status,
56+
sample_project_ids):
5157
datetime_collected = None
5258
# NB a sample may NOT have date and time collected if it has been sent
5359
# out but not yet used
@@ -56,7 +62,8 @@ def from_db(cls, sample_id, date_collected, time_collected,
5662
time_collected)
5763
return cls(sample_id, datetime_collected, site, notes, barcode,
5864
latest_scan_timestamp, latest_sample_information_update,
59-
source_id, account_id, sample_projects, latest_scan_status)
65+
source_id, account_id, sample_projects, latest_scan_status,
66+
sample_project_ids=sample_project_ids)
6067

6168
def to_api(self):
6269
return {

0 commit comments

Comments
 (0)