diff --git a/microsetta_private_api/api/__init__.py b/microsetta_private_api/api/__init__.py index 9dfab34dc..018ffe2cc 100644 --- a/microsetta_private_api/api/__init__.py +++ b/microsetta_private_api/api/__init__.py @@ -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, @@ -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', diff --git a/microsetta_private_api/api/_survey.py b/microsetta_private_api/api/_survey.py index a138df1de..cbd0c8e8a 100644 --- a/microsetta_private_api/api/_survey.py +++ b/microsetta_private_api/api/_survey.py @@ -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, @@ -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, @@ -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}") @@ -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 diff --git a/microsetta_private_api/api/microsetta_private_api.yaml b/microsetta_private_api/api/microsetta_private_api.yaml index ef8386d55..45737c7df 100644 --- a/microsetta_private_api/api/microsetta_private_api.yaml +++ b/microsetta_private_api/api/microsetta_private_api.yaml @@ -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 diff --git a/microsetta_private_api/api/tests/test_api.py b/microsetta_private_api/api/tests/test_api.py index 94cb1f151..132f9dc14 100644 --- a/microsetta_private_api/api/tests/test_api.py +++ b/microsetta_private_api/api/tests/test_api.py @@ -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, @@ -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" @@ -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 @@ -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 diff --git a/microsetta_private_api/db/migration_support.py b/microsetta_private_api/db/migration_support.py index 785ef3ef3..d085ec73c 100644 --- a/microsetta_private_api/db/migration_support.py +++ b/microsetta_private_api/db/migration_support.py @@ -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__, @@ -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 diff --git a/microsetta_private_api/db/patches/0144.sql b/microsetta_private_api/db/patches/0144.sql new file mode 100644 index 000000000..83414e371 --- /dev/null +++ b/microsetta_private_api/db/patches/0144.sql @@ -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); diff --git a/microsetta_private_api/model/sample.py b/microsetta_private_api/model/sample.py index 5b565e262..045efd548 100644 --- a/microsetta_private_api/model/sample.py +++ b/microsetta_private_api/model/sample.py @@ -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 @@ -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. @@ -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 @@ -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 { diff --git a/microsetta_private_api/repo/sample_repo.py b/microsetta_private_api/repo/sample_repo.py index 7e0bb6832..fad2c4218 100644 --- a/microsetta_private_api/repo/sample_repo.py +++ b/microsetta_private_api/repo/sample_repo.py @@ -61,24 +61,36 @@ class SampleRepo(BaseRepo): def __init__(self, transaction): super().__init__(transaction) - def _retrieve_projects(self, sample_barcode): + def _retrieve_projects(self, sample_barcode, return_ids=False): with self._transaction.cursor() as cur: - # If there is a sample, we can look for the projects associated - # with it. We do this as a secondary query: - cur.execute("SELECT barcodes.project.project FROM " - "barcodes.barcode " - "LEFT JOIN " - "barcodes.project_barcode " - "ON " - "barcodes.barcode.barcode = " - "barcodes.project_barcode.barcode " - "LEFT JOIN barcodes.project " - "ON " - "barcodes.project_barcode.project_id = " - "barcodes.project.project_id " - "WHERE " - "barcodes.barcode.barcode = %s", - (sample_barcode,)) + if return_ids is True: + # If the caller wants the project IDs, we can directly + # query the project_barcode table + cur.execute( + "SELECT project_id " + "FROM barcodes.project_barcode " + "WHERE barcode = %s", + (sample_barcode,) + ) + else: + # Otherwise, we defualt to the existing behavior of returning + # the project names. + # If there is a sample, we look for the projects associated + # with it. We do this as a secondary query: + cur.execute("SELECT barcodes.project.project FROM " + "barcodes.barcode " + "LEFT JOIN " + "barcodes.project_barcode " + "ON " + "barcodes.barcode.barcode = " + "barcodes.project_barcode.barcode " + "LEFT JOIN barcodes.project " + "ON " + "barcodes.project_barcode.project_id = " + "barcodes.project.project_id " + "WHERE " + "barcodes.barcode.barcode = %s", + (sample_barcode,)) project_rows = cur.fetchall() sample_projects = [project[0] for project in project_rows] @@ -91,9 +103,13 @@ def _create_sample_obj(self, sample_row): sample_barcode = sample_row[5] scan_timestamp = sample_row[6] sample_projects = self._retrieve_projects(sample_barcode) + sample_project_ids = self._retrieve_projects( + sample_barcode, return_ids=True + ) sample_status = self.get_sample_status(sample_barcode, scan_timestamp) - return Sample.from_db(*sample_row, sample_projects, sample_status) + return Sample.from_db(*sample_row, sample_projects, sample_status, + sample_project_ids=sample_project_ids) # TODO: I'm still not entirely happy with the linking between samples and # sources. The new source_id is direct (and required for environmental diff --git a/microsetta_private_api/repo/survey_template_repo.py b/microsetta_private_api/repo/survey_template_repo.py index fc1f2a511..2dbf7c2e5 100644 --- a/microsetta_private_api/repo/survey_template_repo.py +++ b/microsetta_private_api/repo/survey_template_repo.py @@ -15,6 +15,7 @@ import secrets from microsetta_private_api.exceptions import RepoException from microsetta_private_api.repo.vioscreen_repo import VioscreenRepo +from microsetta_private_api.repo.sample_repo import SampleRepo class SurveyTemplateRepo(BaseRepo): @@ -23,6 +24,7 @@ class SurveyTemplateRepo(BaseRepo): MYFOODREPO_ID = 10002 POLYPHENOL_FFQ_ID = 10003 SPAIN_FFQ_ID = 10004 + SKIN_SCORING_APP_ID = 10005 BASIC_INFO_ID = 10 AT_HOME_ID = 11 LIFESTYLE_ID = 12 @@ -37,6 +39,8 @@ class SurveyTemplateRepo(BaseRepo): COVID19_ID = 21 OTHER_ID = 22 + SBI_COHORT_PROJECT_ID = 162 + SURVEY_INFO = { # For now, let's keep legacy survey info as well. 1: SurveyTemplateLinkInfo( @@ -105,6 +109,12 @@ class SurveyTemplateRepo(BaseRepo): "1.0", "remote" ), + SKIN_SCORING_APP_ID: SurveyTemplateLinkInfo( + SKIN_SCORING_APP_ID, + "Skin Scoring App", + "1.0", + "remote" + ), BASIC_INFO_ID: SurveyTemplateLinkInfo( BASIC_INFO_ID, "Basic Information", @@ -755,6 +765,110 @@ def delete_spain_ffq(self, account_id, source_id): AND source_id=%s""", (account_id, source_id)) + def create_skin_scoring_app_entry(self, + account_id, + source_id): + """Return newly allocated skin scoring app credentials + + Parameters + ---------- + account_id : str, UUID + The account UUID + source_id : str, UUID + The source UUID + + Returns + ------- + str or None + The username allocated to the source, or None if process fails + str or None + The studycode allocated to the source, or None if process fails + """ + + # We need to lock both tables relevant to app credentials + self._transaction.lock_table("skin_scoring_app_credentials") + self._transaction.lock_table("skin_scoring_app_registry") + + with self._transaction.cursor() as cur: + cur.execute( + "SELECT app_username, app_studycode " + "FROM ag.skin_scoring_app_credentials " + "WHERE credentials_allocated = FALSE " + "LIMIT 1" + ) + row = cur.fetchone() + if row is None: + # No credentials are available + return None, None + else: + app_username = row[0] + app_studycode = row[1] + + # Mark the credentials as allocated + cur.execute( + "UPDATE ag.skin_scoring_app_credentials " + "SET credentials_allocated = TRUE " + "WHERE app_username = %s", + (app_username, ) + ) + + # Insert the credentials:source association + cur.execute( + "INSERT INTO ag.skin_scoring_app_registry " + "(app_username, account_id, source_id) " + "VALUES (%s, %s, %s)", + (app_username, account_id, source_id) + ) + + # Put a survey into ag_login_surveys + cur.execute( + "INSERT INTO ag.ag_login_surveys(" + "ag_login_id, survey_id, vioscreen_status, " + "source_id, survey_template_id) " + "VALUES(%s, %s, %s, %s, %s)", + (account_id, app_username, None, + source_id, SurveyTemplateRepo.SKIN_SCORING_APP_ID) + ) + + return app_username, app_studycode + + def get_skin_scoring_app_credentials_if_exists(self, + account_id, + source_id): + """Returns a Skin Scoring App username/studycode set if it exists + + Parameters + ---------- + account_id : str, UUID + The account UUID + source_id : str, UUID + The source UUID + + Returns + ------- + str or None + The associated Skin Scoring App username + str or None + The associated Skin Scoring App studycode + """ + with self._transaction.cursor() as cur: + cur.execute( + """ + SELECT ssac.app_username, ssac.app_studycode + FROM ag.skin_scoring_app_credentials ssac + INNER JOIN ag.skin_scoring_app_registry ssar + ON ssac.app_username = ssar.app_username + WHERE ssar.account_id=%s AND ssar.source_id=%s + """, + (account_id, source_id) + ) + res = cur.fetchone() + + if res is None: + return None, None + else: + return res[0], res[1] + def get_vioscreen_sample_to_user(self): """Obtain a mapping of sample barcode to vioscreen user""" with self._transaction.cursor() as cur: @@ -1127,6 +1241,7 @@ def has_external_surveys(self, account_id, source_id): getters = (self.get_myfoodrepo_id_if_exists, self.get_polyphenol_ffq_id_if_exists, self.get_spain_ffq_id_if_exists, + self.get_skin_scoring_app_credentials_if_exists, self.get_vioscreen_all_ids_if_exists) for get in getters: @@ -1306,7 +1421,8 @@ def _generate_empty_survey(self, survey_template_id, return_tuple=False): if survey_template_id in [self.VIOSCREEN_ID, self.MYFOODREPO_ID, self.POLYPHENOL_FFQ_ID, - self.SPAIN_FFQ_ID]: + self.SPAIN_FFQ_ID, + self.SKIN_SCORING_APP_ID]: raise ValueError("survey_template_id must be for a local " "survey") else: @@ -1401,3 +1517,73 @@ def check_prompt_survey_update( return False return True + + def check_skin_scoring_app_credentials_available(self): + """ Checks whether any username/studycode pairings in the + ag.skin_scoring_app_credentials table are available to allocate + + Returns + ------- + bool + True if credentials are available, otherwise False + """ + with self._transaction.cursor() as cur: + cur.execute( + "SELECT COUNT(app_username) " + "FROM ag.skin_scoring_app_credentials " + "WHERE credentials_allocated = FALSE" + ) + row = cur.fetchone() + return True if row[0] > 0 else False + + def check_display_skin_scoring_app(self, account_id, source_id): + """ Determines whether the skin scoring app external survey should + be included in the survey template IDs returned to the interface. + Two conditions must be met - the source must have a sample + associated with the SBI Sponsored Cohort project and there must be + unallocated credentials available. Any source that has already + been allocated credentials is exempt from those conditions. + + Parameters + ---------- + account_id : str or UUID + The account_id of the source we're checking + source_id : str or UUID + The source_id of the source we're checking + + Returns + ------- + bool + True if the survey should be included, otherwise False + """ + # Check if the participant already has credentials + username, _ = self.get_skin_scoring_app_credentials_if_exists( + account_id, + source_id + ) + + # No existing record, we need to check the two conditions for display + if username is None: + # Check 1 - do they have any samples associated with the SBI + # Cohort project + sample_repo = SampleRepo(self._transaction) + samples = sample_repo.get_samples_by_source(account_id, source_id) + if samples: + has_skin_sample = any( + self.SBI_COHORT_PROJECT_ID + in s.get_project_ids() for s in samples + ) + else: + has_skin_sample = False + + # Check 2 - are any credentials available + credentials = self.check_skin_scoring_app_credentials_available() + + if has_skin_sample and credentials: + return True + else: + return False + + # Existing record, we can return True + else: + return True diff --git a/microsetta_private_api/repo/tests/test_survey_template_repo.py b/microsetta_private_api/repo/tests/test_survey_template_repo.py index 1ed86cfc1..3b97d37cc 100644 --- a/microsetta_private_api/repo/tests/test_survey_template_repo.py +++ b/microsetta_private_api/repo/tests/test_survey_template_repo.py @@ -395,6 +395,111 @@ def test_get_spain_ffq_id_if_exists_false(self): TEST1_SOURCE_ID) self.assertEqual(obs, None) + def test_create_skin_scoring_app_entry_valid(self): + dummy_user = 'test_username1010' + dummy_sc = 'test_studycode1010' + + with Transaction() as t: + with t.cursor() as cur: + # Create a set of credentials to use + cur.execute( + "INSERT INTO ag.skin_scoring_app_credentials " + "(app_username, app_studycode) " + "VALUES (%s, %s)", + (dummy_user, dummy_sc) + ) + + template_repo = SurveyTemplateRepo(t) + obs_u, obs_s = template_repo.create_skin_scoring_app_entry( + TEST1_ACCOUNT_ID, + TEST1_SOURCE_ID, + ) + + # Assert that the function spit out the expected credentials + self.assertEqual(obs_u, dummy_user) + self.assertEqual(obs_s, dummy_sc) + + # Assert that the credentials are marked as allocated + cur.execute( + "SELECT credentials_allocated " + "FROM ag.skin_scoring_app_credentials " + "WHERE app_username = %s", + (dummy_user,) + ) + row = cur.fetchone() + self.assertEqual(row[0], True) + + # Assert that the record is in ag.skin_scoring_app_registry + cur.execute( + "SELECT COUNT(*) " + "FROM ag.skin_scoring_app_registry " + "WHERE app_username = %s AND source_id = %s", + (dummy_user, TEST1_SOURCE_ID) + ) + row = cur.fetchone() + self.assertEqual(row[0], 1) + + def test_create_skin_scoring_app_entry_invalid(self): + with Transaction() as t: + with t.cursor() as cur: + # Mark all credentials as allocated, forcing the creation + # function to fail + cur.execute( + "UPDATE ag.skin_scoring_app_credentials " + "SET credentials_allocated = TRUE" + ) + + template_repo = SurveyTemplateRepo(t) + obs_u, obs_s = template_repo.create_skin_scoring_app_entry( + TEST1_ACCOUNT_ID, + TEST1_SOURCE_ID + ) + self.assertEqual(obs_u, None) + self.assertEqual(obs_s, None) + + def test_get_skin_scoring_app_credentials_if_exists_true(self): + dummy_user = 'test_username1010' + dummy_sc = 'test_studycode1010' + + with Transaction() as t: + with t.cursor() as cur: + # Create a set of credentials to use + cur.execute( + "INSERT INTO ag.skin_scoring_app_credentials " + "(app_username, app_studycode) " + "VALUES (%s, %s)", + (dummy_user, dummy_sc) + ) + + template_repo = SurveyTemplateRepo(t) + test_ssa_u, test_ssa_s =\ + template_repo.create_skin_scoring_app_entry( + TEST1_ACCOUNT_ID, TEST1_SOURCE_ID, + ) + + # Assert that we didn't just get None, None back + self.assertNotEqual(test_ssa_u, None) + self.assertNotEqual(test_ssa_s, None) + + obs_ssa_u, obs_ssa_s =\ + template_repo.get_skin_scoring_app_credentials_if_exists( + TEST1_ACCOUNT_ID, TEST1_SOURCE_ID + ) + + # Assert that we're getting the same credentials back + self.assertEqual(test_ssa_u, obs_ssa_u) + self.assertEqual(test_ssa_s, obs_ssa_s) + + def test_get_skin_scoring_app_credentials_if_exists_false(self): + with Transaction() as t: + template_repo = SurveyTemplateRepo(t) + obs_u, obs_s =\ + template_repo.get_skin_scoring_app_credentials_if_exists( + TEST1_ACCOUNT_ID, TEST1_SOURCE_ID + ) + self.assertEqual(obs_u, None) + self.assertEqual(obs_s, None) + def test_create_vioscreen_id_valid(self): with Transaction() as t: template_repo = SurveyTemplateRepo(t) @@ -751,6 +856,205 @@ def test_check_prompt_survey_update(self): obs = s_t_r.check_prompt_survey_update(source_id) self.assertTrue(obs) + def test_check_skin_scoring_app_credentials_available(self): + with Transaction() as t: + str = SurveyTemplateRepo(t) + with t.cursor() as cur: + # Insert a new credential pairing to ensure we have one record + # that's not allocated + cur.execute( + "INSERT INTO ag.skin_scoring_app_credentials " + "(app_username, app_studycode) " + "VALUES ('microsettafoo','bar')" + ) + + # And confirm that the function to check availability + # returns True + obs = str.check_skin_scoring_app_credentials_available() + self.assertTrue(obs) + + # Now, update all records to be allocated + cur.execute( + "UPDATE ag.skin_scoring_app_credentials " + "SET credentials_allocated = TRUE" + ) + + # And confirm that the function to check availability + # returns False + obs = str.check_skin_scoring_app_credentials_available() + self.assertFalse(obs) + + def test_check_display_skin_scoring_app_true(self): + dummy_user = 'test_username1010' + dummy_sc = 'test_studycode1010' + with Transaction() as t: + with t.cursor() as cur: + # Create a set of credentials to use + cur.execute( + "INSERT INTO ag.skin_scoring_app_credentials " + "(app_username, app_studycode) " + "VALUES (%s, %s)", + (dummy_user, dummy_sc) + ) + + str = SurveyTemplateRepo(t) + + # Scenario 1 - the participant does not have a username, but they + # have a sample in the SBI Sponsored cohort and credentials are + # available to allocate + + # Assert that they do not have a username + obs_u, obs_s =\ + str.get_skin_scoring_app_credentials_if_exists( + TEST1_ACCOUNT_ID, TEST1_SOURCE_ID + ) + self.assertEqual(obs_u, None) + self.assertEqual(obs_s, None) + + # Associate their sample with SBI + self._associate_sample_with_sbi(TEST1_SAMPLE_ID, True, t) + + # And confirm that we should display the app to the source + obs = str.check_display_skin_scoring_app( + TEST1_ACCOUNT_ID, + TEST1_SOURCE_ID + ) + self.assertTrue(obs) + + # Scenario 2 - the participant already has a username + + # Allocate the participant a set of credentials + ssa_u, ssa_s = str.create_skin_scoring_app_entry( + TEST1_ACCOUNT_ID, TEST1_SOURCE_ID + ) + self.assertNotEqual(ssa_u, None) + self.assertNotEqual(ssa_s, None) + + # Mark all other credentials as allocated + with t.cursor() as cur: + cur.execute( + "UPDATE ag.skin_scoring_app_credentials " + "SET credentials_allocated = TRUE" + ) + + # And confirm that we should still display the app + obs = str.check_display_skin_scoring_app( + TEST1_ACCOUNT_ID, + TEST1_SOURCE_ID + ) + self.assertTrue(obs) + + def test_check_display_skin_scoring_app_false(self): + dummy_user = 'test_username1010' + dummy_sc = 'test_studycode1010' + with Transaction() as t: + with t.cursor() as cur: + # Create a set of credentials to use + cur.execute( + "INSERT INTO ag.skin_scoring_app_credentials " + "(app_username, app_studycode) " + "VALUES (%s, %s)", + (dummy_user, dummy_sc) + ) + + str = SurveyTemplateRepo(t) + + # Scenario 1 - credentials are available, but the participant + # doesn't have a sample in the SBI Sponsored cohort + + # Make sure they don't have a sample associated with SBI + self._associate_sample_with_sbi(TEST1_SAMPLE_ID, False, t) + + # And confirm that we shouldn't display the app + obs = str.check_display_skin_scoring_app( + TEST1_ACCOUNT_ID, + TEST1_SOURCE_ID + ) + self.assertFalse(obs) + + # Scenario 2 - the participant has a sample in the SBI Sponsored + # cohort, but no credentials are available + + # Associate their sample with SBI + self._associate_sample_with_sbi(TEST1_SAMPLE_ID, True, t) + + # But mark all credentials as allocated + with t.cursor() as cur: + cur.execute( + "UPDATE ag.skin_scoring_app_credentials " + "SET credentials_allocated = TRUE" + ) + + # And confirm that we shouldn't display the app + obs = str.check_display_skin_scoring_app( + TEST1_ACCOUNT_ID, + TEST1_SOURCE_ID + ) + self.assertFalse(obs) + + def _associate_sample_with_sbi(self, sample_id, project_state, t): + # Helper function to temporarily associate a sample with SBI. + with t.cursor() as cur: + # NB: The real SBI cohort project doesn't exist in the development + # database. For the sake of not breaking these tests, we're going + # to check if it exists. If it doesn't, we'll create it. + cur.execute( + "SELECT COUNT(*) " + "FROM barcodes.project " + "WHERE project_id = %s", + (SurveyTemplateRepo.SBI_COHORT_PROJECT_ID,) + ) + row = cur.fetchone() + if row[0] == 0: + cur.execute( + "INSERT INTO barcodes.project " + "(project_id, project, is_microsetta, bank_samples)" + "VALUES (%s, 'SBI', %s, %s)", + (SurveyTemplateRepo.SBI_COHORT_PROJECT_ID, True, True) + ) + + cur.execute( + "SELECT barcode " + "FROM ag.ag_kit_barcodes " + "WHERE ag_kit_barcode_id = %s", + (sample_id, ) + ) + row = cur.fetchone() + barcode = row[0] + + if project_state is True: + # NB: Developing and visual testing is easiest with + # SBI_COHORT_PROJECT_ID set to 1 to leverage existing + # test data. If it's set that way, we don't need to insert + # a new record. + cur.execute( + "SELECT COUNT(*) " + "FROM barcodes.project_barcode " + "WHERE project_id = %s AND barcode = %s", + (SurveyTemplateRepo.SBI_COHORT_PROJECT_ID, + barcode) + ) + row = cur.fetchone() + + if row[0] == 0: + cur.execute( + "INSERT INTO barcodes.project_barcode " + "(project_id, barcode) " + "VALUES (%s, %s)", + (SurveyTemplateRepo.SBI_COHORT_PROJECT_ID, + barcode) + ) + else: + # Delete any association between the barcode and the SBI + # project ID. This could leave the barcode with no project + # associations, but it's immaterial for these unit tests. + cur.execute( + "DELETE FROM barcodes.project_barcode " + "WHERE project_id = %s AND barcode = %s", + (SurveyTemplateRepo.SBI_COHORT_PROJECT_ID, + barcode) + ) + filled_surveys = { "10": { "22": "I am right handed", diff --git a/microsetta_private_api/server_config.json b/microsetta_private_api/server_config.json index d49f8a7bd..aa0790861 100644 --- a/microsetta_private_api/server_config.json +++ b/microsetta_private_api/server_config.json @@ -46,5 +46,7 @@ "google_geocoding_url": "https://maps.googleapis.com/maps/api/geocode/json", "google_geocoding_key": "geocoding_key_placeholder", "japanese_ffqs_path_key": "/tmp/japanese_ffqs/key.csv", - "japanese_ffqs_path_reports": "/tmp/japanese_ffqs/" + "japanese_ffqs_path_reports": "/tmp/japanese_ffqs/", + "skin_app_credentials_path": "skin_app_credentials_placeholder", + "skin_app_url": "skin_app_url_placeholder" }