Skip to content

Commit

Permalink
Merge pull request #600 from biocore/csymons_skin_scoring_app_working
Browse files Browse the repository at this point in the history
Skin Scoring App
  • Loading branch information
cassidysymons authored Feb 12, 2025
2 parents 5287a90 + 2c97fd3 commit 61d7e1b
Show file tree
Hide file tree
Showing 11 changed files with 774 additions and 47 deletions.
5 changes: 4 additions & 1 deletion microsetta_private_api/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
read_survey_template, read_survey_templates, read_answered_survey,
read_answered_surveys, submit_answered_survey,
read_answered_survey_associations, top_food_report,
read_myfoodrepo_available_slots
read_myfoodrepo_available_slots, get_skin_scoring_app_credentials,
post_skin_scoring_app_credentials
)
from ._sample import (
read_sample_association, associate_sample, read_sample_associations,
Expand Down Expand Up @@ -103,6 +104,8 @@
'read_answered_survey_associations',
'top_food_report',
'read_myfoodrepo_available_slots',
'get_skin_scoring_app_credentials',
'post_skin_scoring_app_credentials',
'read_sample_association',
'associate_sample',
'read_sample_associations',
Expand Down
125 changes: 106 additions & 19 deletions microsetta_private_api/api/_survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,40 @@ def read_survey_templates(account_id, source_id, language_tag, token_info):
with Transaction() as t:
source_repo = SourceRepo(t)
source = source_repo.get_source(account_id, source_id)

if source is None:
return jsonify(code=404, message="No source found"), 404

template_repo = SurveyTemplateRepo(t)

if source.source_type == Source.SOURCE_TYPE_HUMAN:
return jsonify([template_repo.get_survey_template_link_info(x)
for x in [
SurveyTemplateRepo.VIOSCREEN_ID,
SurveyTemplateRepo.POLYPHENOL_FFQ_ID,
SurveyTemplateRepo.SPAIN_FFQ_ID,
SurveyTemplateRepo.BASIC_INFO_ID,
SurveyTemplateRepo.AT_HOME_ID,
SurveyTemplateRepo.LIFESTYLE_ID,
SurveyTemplateRepo.GUT_ID,
SurveyTemplateRepo.GENERAL_HEALTH_ID,
SurveyTemplateRepo.HEALTH_DIAG_ID,
SurveyTemplateRepo.ALLERGIES_ID,
SurveyTemplateRepo.DIET_ID,
SurveyTemplateRepo.DETAILED_DIET_ID,
SurveyTemplateRepo.OTHER_ID
]]), 200
template_ids = [
SurveyTemplateRepo.VIOSCREEN_ID,
SurveyTemplateRepo.POLYPHENOL_FFQ_ID,
SurveyTemplateRepo.SPAIN_FFQ_ID,
SurveyTemplateRepo.BASIC_INFO_ID,
SurveyTemplateRepo.AT_HOME_ID,
SurveyTemplateRepo.LIFESTYLE_ID,
SurveyTemplateRepo.GUT_ID,
SurveyTemplateRepo.GENERAL_HEALTH_ID,
SurveyTemplateRepo.HEALTH_DIAG_ID,
SurveyTemplateRepo.ALLERGIES_ID,
SurveyTemplateRepo.DIET_ID,
SurveyTemplateRepo.DETAILED_DIET_ID,
SurveyTemplateRepo.OTHER_ID
]
if template_repo.check_display_skin_scoring_app(
account_id, source_id
):
template_ids.append(SurveyTemplateRepo.SKIN_SCORING_APP_ID)

elif source.source_type == Source.SOURCE_TYPE_ANIMAL:
return jsonify([template_repo.get_survey_template_link_info(x)
for x in [2]]), 200
template_ids = [2]
else:
return jsonify([]), 200
template_ids = []

return jsonify([template_repo.get_survey_template_link_info(x)
for x in template_ids]), 200


def _remote_survey_url_vioscreen(transaction, account_id, source_id,
Expand Down Expand Up @@ -181,6 +190,23 @@ def _remote_survey_url_spain_ffq(transaction, account_id, source_id):
return SERVER_CONFIG['spain_ffq_url']


def _remote_survey_url_skin_scoring_app(transaction,
account_id,
source_id):
st_repo = SurveyTemplateRepo(transaction)

# Confirm that the user has credentials allocated
ssa_u, _ = st_repo.get_skin_scoring_app_credentials_if_exists(
account_id,
source_id
)

if ssa_u is None:
raise NotFound("Sorry, you were not allocated credentials")

return SERVER_CONFIG['skin_app_url']


def read_survey_template(account_id, source_id, survey_template_id,
language_tag, token_info, survey_redirect_url=None,
vioscreen_ext_sample_id=None,
Expand Down Expand Up @@ -220,6 +246,11 @@ def read_survey_template(account_id, source_id, survey_template_id,
url = _remote_survey_url_spain_ffq(t,
account_id,
source_id)
elif survey_template_id == \
SurveyTemplateRepo.SKIN_SCORING_APP_ID:
url = _remote_survey_url_skin_scoring_app(t,
account_id,
source_id)
else:
raise ValueError(f"Cannot generate URL for survey "
f"{survey_template_id}")
Expand Down Expand Up @@ -499,3 +530,59 @@ def read_myfoodrepo_available_slots():
resp = jsonify(code=200, number_of_available_slots=available,
total_number_of_slots=total)
return resp, 200


def get_skin_scoring_app_credentials(account_id, source_id, token_info):
_validate_account_access(token_info, account_id)

with Transaction() as t:
st_repo = SurveyTemplateRepo(t)
ssa_u, ssa_s = st_repo.get_skin_scoring_app_credentials_if_exists(
account_id, source_id
)
response_obj = {
"app_username": ssa_u,
"app_studycode": ssa_s
}
return jsonify(response_obj), 200


def post_skin_scoring_app_credentials(account_id, source_id, token_info):
_validate_account_access(token_info, account_id)

with Transaction() as t:
st_repo = SurveyTemplateRepo(t)

# First, confirm that the source doesn't already have credentials
ssa_u, _ = st_repo.get_skin_scoring_app_credentials_if_exists(
account_id, source_id
)

# This shouldn't happen, but if it does, return an error
if ssa_u is not None:
return jsonify(
code=400,
message="Credentials already exist"
), 400

# Now, try to allocate credentials and create an entry in the skin
# scoring app registry table
ssa_u, ssa_s = st_repo.create_skin_scoring_app_entry(
account_id, source_id
)
t.commit()

if ssa_u is None:
# No credentials were available
return jsonify(
code=404,
message="No credentials available"
), 404
else:
# Credentials were successfully allocated
return jsonify(
{
"app_username": ssa_u,
"app_studycode": ssa_s
}
), 201
58 changes: 58 additions & 0 deletions microsetta_private_api/api/microsetta_private_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,64 @@ paths:
'404':
$ref: '#/components/responses/404NotFound'

'/accounts/{account_id}/sources/{source_id}/surveys/skin_scoring_app_credentials':
get:
operationId: microsetta_private_api.api.get_skin_scoring_app_credentials
tags:
- Surveys (By Source)
summary: Get skin scoring app credentials associated with source, if they exist
description: Get skin scoring app credentials associated with source, if they exist
parameters:
- $ref: '#/components/parameters/account_id'
- $ref: '#/components/parameters/source_id'
responses:
'200':
description: Credentials for skin scoring app
content:
application/json:
schema:
type: object
properties:
app_username:
type: string
nullable: true
app_studycode:
type: string
nullable: true
'401':
$ref: '#/components/responses/401Unauthorized'
'403':
$ref: '#/components/responses/403Forbidden'
post:
operationId: microsetta_private_api.api.post_skin_scoring_app_credentials
tags:
- Surveys (By Source)
summary: Create association between a set of skin scoring app credentials and a source
description: Create association between a set of skin scoring app credentials and a source
parameters:
- $ref: '#/components/parameters/account_id'
- $ref: '#/components/parameters/source_id'
responses:
'201':
description: Credentials for skin scoring app
content:
application/json:
schema:
type: object
properties:
app_username:
type: string
app_studycode:
type: string
'400':
description: 'Credentials already exist for source'
'401':
$ref: '#/components/responses/401Unauthorized'
'403':
$ref: '#/components/responses/403Forbidden'
'404':
$ref: '#/components/responses/404NotFound'

'/accounts/{account_id}/sources/{source_id}/samples':
get:
operationId: microsetta_private_api.api.read_sample_associations
Expand Down
13 changes: 10 additions & 3 deletions microsetta_private_api/api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@
'sample_projects': ['American Gut Project'],
'account_id': None,
'source_id': None,
'sample_site': None}
'sample_site': None,
'sample_project_ids': [1]}

DUMMY_FILLED_SAMPLE_INFO = {
'sample_barcode': BARCODE,
Expand All @@ -195,7 +196,8 @@
'sample_projects': ['American Gut Project'],
'account_id': 'foobar',
'source_id': 'foobarbaz',
'sample_site': 'Saliva'}
'sample_site': 'Saliva',
'sample_project_ids': [1]}

ACCT_ID_KEY = "account_id"
ACCT_TYPE_KEY = "account_type"
Expand Down Expand Up @@ -594,7 +596,8 @@ def create_dummy_sample_objects(filled=False):
info_dict['account_id'],
None,
info_dict["sample_projects"],
None)
None,
sample_project_ids=info_dict["sample_project_ids"])

return sample_info, sample
# endregion help methods
Expand Down Expand Up @@ -2244,6 +2247,10 @@ def test_associate_sample_to_source_success(self):
exp['account_id'] = ACCT_ID_1
exp['kit_id'] = None

# Remove the sample_project_ids element since we don't expect that
# to come out of the API
exp.pop("sample_project_ids")

self.assertEqual(get_resp_obj, [exp])

# TODO: We should also have tests of associating a sample to a source
Expand Down
32 changes: 31 additions & 1 deletion microsetta_private_api/db/migration_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,35 @@ def migrate_133(TRN):
print("No mapping: " + ffq_id + " - " + barcode)
TRN.execute()

@staticmethod
def migrate_144(TRN):
# We need to load the credentials that the vendor provided in CSV form
# Format is username, studycode and includes a header row
skin_app_credentials_path = SERVER_CONFIG["skin_app_credentials_path"]
if not os.path.exists(skin_app_credentials_path):
print(
"Credentials for app not found:" + skin_app_credentials_path
)
return

with open(skin_app_credentials_path) as csv_file:
csv_contents = csv.reader(csv_file)
header = True

for csv_row in csv_contents:
if header:
header = False
continue
app_username, app_studycode = csv_row

TRN.add(
"INSERT INTO ag.skin_scoring_app_credentials "
"(app_username, app_studycode) "
"VALUES (%s, %s)",
(app_username, app_studycode)
)
TRN.execute()

MIGRATION_LOOKUP = {
"0048.sql": migrate_48.__func__,
"0050.sql": migrate_50.__func__,
Expand All @@ -806,7 +835,8 @@ def migrate_133(TRN):
# "0082.sql": migrate_82.__func__
# ...
"0096.sql": migrate_96.__func__,
"0133.sql": migrate_133.__func__
"0133.sql": migrate_133.__func__,
"0144.sql": migrate_144.__func__
}

@classmethod
Expand Down
27 changes: 27 additions & 0 deletions microsetta_private_api/db/patches/0144.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- The organization that hosts the skin-scoring app provides us with username
-- and studycode pairings for participants to access the app. We need to store
-- these pairings, as well as a flag for whether the pairing has been
-- allocated to a participant. We explicitly store this flag to avoid reuse
-- if sources (and their related survey databsase records) were to be deleted.
CREATE TABLE ag.skin_scoring_app_credentials (
app_username VARCHAR PRIMARY KEY,
app_studycode VARCHAR NOT NULL,
credentials_allocated BOOLEAN NOT NULL DEFAULT FALSE
);

-- And we create a registry table, similar to all of the other external
-- surveys we've hosted in the past, to link the username to the account and
-- source that used it.
CREATE TABLE ag.skin_scoring_app_registry (
app_username VARCHAR PRIMARY KEY,
account_id UUID NOT NULL,
source_id UUID,
deleted BOOLEAN NOT NULL DEFAULT false,
creation_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),

CONSTRAINT fk_skin_scoring_app_username FOREIGN KEY (app_username) REFERENCES ag.skin_scoring_app_credentials(app_username),
CONSTRAINT fk_skin_scoring_app_registry_account FOREIGN KEY (account_id) REFERENCES ag.account(id),
CONSTRAINT fk_skin_scoring_app_registry_source FOREIGN KEY (source_id) REFERENCES ag.source(id)
);

CREATE INDEX skin_scoring_app_registry_source ON ag.skin_scoring_app_registry (account_id, source_id);
13 changes: 10 additions & 3 deletions microsetta_private_api/model/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class Sample(ModelBase):
def __init__(self, sample_id, datetime_collected, site, notes, barcode,
latest_scan_timestamp, source_id, account_id,
latest_sample_information_update, sample_projects,
latest_scan_status, kit_id=None):
latest_scan_status, kit_id=None, sample_project_ids=None):
self.id = sample_id
# NB: datetime_collected may be None if sample not yet used
self.datetime_collected = datetime_collected
Expand All @@ -28,9 +28,14 @@ def __init__(self, sample_id, datetime_collected, site, notes, barcode,
self.accession_urls = []
self.kit_id = kit_id

self._sample_project_ids = sample_project_ids

def set_accession_urls(self, accession_urls):
self.accession_urls = accession_urls

def get_project_ids(self):
return self._sample_project_ids

@property
def edit_locked(self):
# If a sample has been scanned and is valid, it is locked.
Expand All @@ -47,7 +52,8 @@ def remove_locked(self):
def from_db(cls, sample_id, date_collected, time_collected,
site, notes, barcode, latest_scan_timestamp,
latest_sample_information_update, source_id,
account_id, sample_projects, latest_scan_status):
account_id, sample_projects, latest_scan_status,
sample_project_ids):
datetime_collected = None
# NB a sample may NOT have date and time collected if it has been sent
# out but not yet used
Expand All @@ -56,7 +62,8 @@ def from_db(cls, sample_id, date_collected, time_collected,
time_collected)
return cls(sample_id, datetime_collected, site, notes, barcode,
latest_scan_timestamp, latest_sample_information_update,
source_id, account_id, sample_projects, latest_scan_status)
source_id, account_id, sample_projects, latest_scan_status,
sample_project_ids=sample_project_ids)

def to_api(self):
return {
Expand Down
Loading

0 comments on commit 61d7e1b

Please sign in to comment.