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 #339

Merged
merged 11 commits into from
Feb 12, 2025
75 changes: 68 additions & 7 deletions microsetta_interface/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class Source:
VIOSCREEN_ID = 10001
MYFOODREPO_ID = 10002
POLYPHENOL_FFQ_ID = 10003
SKIN_SCORING_APP_ID = 10005
SPAIN_FFQ_ID = 10004

SYSTEM_MSG_DICTIONARY = {
Expand Down Expand Up @@ -398,6 +399,21 @@ class Source:
'est_minutes': '30',
'icon': 'survey_external.svg'
},
SKIN_SCORING_APP_ID: {
'description': 'This will direct you to the ModiFace skin-scoring web'
' app. This app allows you to upload a selfie photo, '
'which will be used to generate anonymized data about '
'your skin for researchers and provide you with what '
'the algorithm assesses to be your top two skin '
'concerns. You will be provided a username and study'
' code on the next screen to access the app, which '
'will link your ModiFace results to your skin sample. '
'This app is hosted by a third-party provider; we are '
'unable to provide any assistance if you encounter '
'errors or issues while using the app.',
'est_minutes': '5',
'icon': 'survey_external.svg'
},
}
LOCAL_SURVEY_SEQUENCE = [
BASIC_INFO_ID,
Expand Down Expand Up @@ -466,7 +482,8 @@ def _get_req_survey_templates_by_source_type(source_type):

def _get_opt_survey_templates_by_source_type(source_type):
if source_type == Source.SOURCE_TYPE_HUMAN:
return [3, 4, 5, 7, MYFOODREPO_ID, POLYPHENOL_FFQ_ID, SPAIN_FFQ_ID]
return [3, 4, 5, 7, MYFOODREPO_ID, POLYPHENOL_FFQ_ID,
SPAIN_FFQ_ID, SKIN_SCORING_APP_ID]
elif source_type == Source.SOURCE_TYPE_ANIMAL:
return []
elif source_type == Source.SOURCE_TYPE_ENVIRONMENT:
Expand Down Expand Up @@ -1398,6 +1415,14 @@ def get_fill_source_survey(*,
account_id, source_id, "data", reconsent=True
)

# this is remote, so go to an external url, not our jinja2 template
return redirect(survey_output['survey_template_text']['url'])
elif survey_template_id == SKIN_SCORING_APP_ID:
if need_reconsent:
return render_consent_page(
account_id, source_id, "data", reconsent=True
)

# this is remote, so go to an external url, not our jinja2 template
return redirect(survey_output['survey_template_text']['url'])
else:
Expand Down Expand Up @@ -1542,6 +1567,25 @@ def get_myfoodrepo_no_slots(*, account_id=None, source_id=None):
source_id=source_id)


@prerequisite([SOURCE_PREREQS_MET, BIOSPECIMEN_PREREQS_MET])
def post_ajax_skin_scoring_app_credentials(*, account_id, source_id):
need_reconsent = check_current_consent(account_id, source_id, "data")

if need_reconsent:
return render_consent_page(
account_id, source_id, "data", reconsent=True
)

has_error, credentials, _ = ApiRequest.post(
"/accounts/%s/sources/%s/surveys/skin_scoring_app_credentials"
% (account_id, source_id)
)
if has_error == 404:
return flask.jsonify({"app_username": "", "app_studycode": ""})
else:
return flask.jsonify(credentials)


@prerequisite([SOURCE_PREREQS_MET, BIOSPECIMEN_PREREQS_MET])
def get_fill_vioscreen_remote_sample_survey(*,
account_id=None,
Expand Down Expand Up @@ -1850,7 +1894,10 @@ def get_source(*, account_id=None, source_id=None):
for answer in survey_answers:
template_id = answer['survey_template_id']
for template in local_surveys + remote_surveys:
if template['survey_template_id'] == template_id:
if template['survey_template_id'] == SKIN_SCORING_APP_ID:
template['survey_id'] = answer['survey_id']
template['answered'] = True
else:
template['answered'] = True

for template in local_surveys:
Expand Down Expand Up @@ -1879,11 +1926,24 @@ def get_source(*, account_id=None, source_id=None):
template['est_minutes'] = SURVEY_INFO[template_id]['est_minutes']
template['icon'] = SURVEY_INFO[template_id]['icon']

# TODO: MyFoodRepo logic needs to be refactored when we reactivate it
"""
# any survey specific stuff like opening a tab
# or slot checking
for idx, template in enumerate(remote_surveys[:]):
# NB: change "_" back to "idx" when MyFoodRepo is reactivated or if
# another external survey requires similar functionality
for _, template in enumerate(remote_surveys[:]):
if template['survey_template_id'] == SKIN_SCORING_APP_ID:
has_error, credentials, _ = ApiRequest.get(
'/accounts/%s/sources/%s/surveys/skin_scoring_app_credentials'
% (account_id, source_id)
)

if has_error:
return has_error

template['credentials'] = credentials

# TODO: MyFoodRepo logic needs to be refactored when we reactivate it
"""
if template['survey_template_id'] == MYFOODREPO_ID:
has_error, slots, _ = ApiRequest.get('/slots/myfoodrepo')
if has_error:
Expand All @@ -1896,7 +1956,7 @@ def get_source(*, account_id=None, source_id=None):
per_source_not_taken.pop(idx)
else:
template['new_tab'] = False
"""
"""

local_surveys = [translate_survey_template(s) for s in local_surveys]
remote_surveys = [translate_survey_template(s) for s in remote_surveys]
Expand All @@ -1911,7 +1971,8 @@ def get_source(*, account_id=None, source_id=None):
source_name=source_output['source_name'],
profile_has_samples=profile_has_samples,
need_reconsent=need_reconsent,
show_update_age=show_update_age
show_update_age=show_update_age,
SKIN_SCORING_APP_ID=SKIN_SCORING_APP_ID
)


Expand Down
16 changes: 16 additions & 0 deletions microsetta_interface/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,22 @@ paths:
'302':
description: Redirecting to necessary action

'/accounts/{account_id}/sources/{source_id}/create_skin_scoring_app_credentials':
post:
operationId: microsetta_interface.implementation.post_ajax_skin_scoring_app_credentials
tags:
- Survey
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

'/accounts/{account_id}/sources/{source_id}/vioscreen_ffq':
get:
operationId: microsetta_interface.implementation.get_fill_vioscreen_remote_sample_survey
Expand Down
106 changes: 102 additions & 4 deletions microsetta_interface/templates/source.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,74 @@
}
{% endif %}

let skin_app_username = '';
let skin_app_studycode = '';

function handleSkinAppModal(survey_link, survey_title, survey_description, app_username, app_studycode) {
$("#skin-app-modal-title").empty();
$("#skin-app-modal-title").text(survey_title);
$("#skin-app-survey-description").empty();
$("#skin-app-survey-description").text(survey_description);

// We allow the function to update the global variables, which will happen when the user receives credentials and re-renders the modal.
// However, there's no situation where we'd retract the credentials, so we don't need to do anything with blank values.
if(app_username != '' && app_username != 'None') {
skin_app_username = app_username
skin_app_studycode = app_studycode
}

if(skin_app_username != '') {
$("#skin-app-take-survey").html('{{ _('Take This Survey Now') }}');

document.getElementById("app-credentials-div").style.display = '';
document.getElementById("app-credentials-username").innerHTML = skin_app_username;
document.getElementById("app-credentials-studycode").innerHTML = skin_app_studycode;

// Once we display the credentials, the button action is to open the actual survey
$("#skin-app-take-survey").click(function(e) {
window.open(survey_link, '_blank').focus();
});
} else {
$("#skin-app-take-survey").html('{{ _('Get Username and Study Code') }}');

document.getElementById("app-credentials-div").style.display = 'none';
$("#skin-app-take-survey").click(async function(e) {
let url = '/accounts/{{ account_id }}/sources/{{ source_id }}/create_skin_scoring_app_credentials';
$.ajax({
url: url,
type: "POST",
success: function(data) {
if(!Object.hasOwn(data, 'app_username')) {
// The response doesn't have credentials, something went very wrong. Handle it gracefully.
document.getElementById("app-error").style.display = '';
} else {
let s_user = data['app_username'];
let s_sc = data['app_studycode'];
if (s_user.length < 1) {
// We weren't able to allocate them credentials.
document.getElementById("app-error").style.display = '';
} else {
// We got credentials, refresh this modal
handleSkinAppModal(survey_link, survey_title, survey_description, s_user, s_sc);
}
}
}
});
});
}
$("#skin_app_modal").modal('show');
return false;
}

function hideModal() {
$("#source_modal").modal('hide');
return false;
}

function hideSkinModal() {
$("#skin_app_modal").modal('hide');
return false;
}
</script>
<script src="/static/vendor/js/jquery.form-4.2.2/jquery.form.min.js"></script>
{% endblock %}
Expand Down Expand Up @@ -123,8 +187,12 @@
<div class="row mt-4">
{% for detail in remote_surveys %}
<div class="col-12 col-sm-6 col-md-4 p-2">
{% if not detail.answered %}
<a style="text-decoration: none" onClick="return takeExternalSurvey('/accounts/{{ account_id }}/sources/{{ source_id }}/take_survey?survey_template_id={{ detail.survey_template_id }}', '{{ detail.survey_template_title }}', '{{ detail.description }}');" href="/accounts/{{ account_id }}/sources/{{ source_id }}/take_survey?survey_template_id={{ detail.survey_template_id }}" target="_blank">
{% if detail.survey_template_id == SKIN_SCORING_APP_ID %}
<a style="text-decoration: none" onClick="return handleSkinAppModal('/accounts/{{ account_id }}/sources/{{ source_id }}/take_survey?survey_template_id={{ detail.survey_template_id }}', '{{ detail.survey_template_title }}', '{{ detail.description }}', '{{ detail.credentials['app_username'] }}', '{{ detail.credentials['app_studycode'] }}');" href="/accounts/{{ account_id }}/sources/{{ source_id }}/take_survey?survey_template_id={{ detail.survey_template_id }}" target="_blank">
{% else %}
{% if not detail.answered %}
<a style="text-decoration: none" onClick="return takeExternalSurvey('/accounts/{{ account_id }}/sources/{{ source_id }}/take_survey?survey_template_id={{ detail.survey_template_id }}', '{{ detail.survey_template_title }}', '{{ detail.description }}');" href="/accounts/{{ account_id }}/sources/{{ source_id }}/take_survey?survey_template_id={{ detail.survey_template_id }}" target="_blank">
{% endif %}
{% endif %}
<div class="card card-survey-external">
<div class="row m-2 survey-info-row">
Expand All @@ -136,7 +204,7 @@
<div class="col-4 small-text survey-info text-end">{{ detail.est_minutes }} {{ _('min') }}</div>
</div>
<div class="row m-3 text-center survey-title">
{% if not detail.answered %}
{% if not detail.answered or detail.survey_template_id == SKIN_SCORING_APP_ID %}
<img src="/static/img/survey_external.svg" class="card-survey-icon-external">
{% else %}
<img src="/static/img/survey_external_taken.svg" class="card-survey-icon-external">
Expand All @@ -150,7 +218,7 @@
{% endif %}
</div>
</div>
{% if not detail.answered %}
{% if not detail.answered or detail.survey_template_id == SKIN_SCORING_APP_ID %}
</a>
{% endif %}
</div>
Expand Down Expand Up @@ -179,4 +247,34 @@
</div>
</div>

<! -- Modal for skin-scoring app -->
<div class="modal fade" id="skin_app_modal" tabindex="-1" role="dialog" aria-labelledby="skinAppModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<img src="/static/img/survey_external.svg">
<span class="survey-title" id="skin-app-modal-title"></span>
</div>
<div class="modal-body">
<p class="survey-description" id="skin-app-survey-description"></p>
</div>
<div class="modal-body" id="app-credentials-div">
<p class="survey-description">
<strong>Username: </strong><span id="app-credentials-username"></span><br />
<strong>Study Code: </strong><span id="app-credentials-studycode"></span>
</p>
</div>
<div class="modal-body" id="app-error" style="display: none">
<p class="survey-description">
{{ _('Sorry, there was a problem generating a username and study code. Please try again later.') }}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-white-blue-border" id="skin_app_cancel" onClick="hideSkinModal();">{{ _('Maybe Later') }}</button>
<button type="button" class="btn btn-blue-gradient" id="skin-app-take-survey">{{ _('Take This Survey Now') }}</button>
</div>
</div>
</div>
</div>

{% endblock %}
6 changes: 6 additions & 0 deletions microsetta_interface/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def _fake_jwt(email, verified, uniqify=False):
MYFOODREPO_ID = 10002
POLYPHENOL_FFQ_ID = 10003
SPAIN_FFQ_ID = 10004
SKIN_SCORING_APP_ID = 10005

BASIC_INFO_SIMPLE = {"112": "1970"}
BASIC_INFO_SIMPLE_ALT = {"112": "1983"}
Expand Down Expand Up @@ -573,6 +574,11 @@ def _complete_spain_ffq_survey(self, account_id, source_id):
f'take_survey?survey_template_id=10004')
return self.app.get(url), url

def _complete_skin_scoring_app_survey(self, account_id, source_id):
url = (f'/accounts/{account_id}/sources/{source_id}/'
f'take_survey?survey_template_id={SKIN_SCORING_APP_ID}')
return self.app.get(url), url

def test_new_user_to_source_listing(self):
resp, url, user_jwt = self._new_to_create()
account_id, _, _ = self._ids_from_url(url)
Expand Down