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

Skin Scoring App #600

Merged
merged 15 commits into from
Feb 12, 2025
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':
AmandaBirmingham marked this conversation as resolved.
Show resolved Hide resolved
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