diff --git a/ci/conda_requirements.txt b/ci/conda_requirements.txt index 082c3130..620d545a 100644 --- a/ci/conda_requirements.txt +++ b/ci/conda_requirements.txt @@ -1,5 +1,5 @@ flake8 -flask +flask < 3.0.0 natsort pycryptodome redis-py diff --git a/microsetta_interface/implementation.py b/microsetta_interface/implementation.py index 283b6755..978d3fc7 100644 --- a/microsetta_interface/implementation.py +++ b/microsetta_interface/implementation.py @@ -1144,12 +1144,20 @@ def get_account(*, account_id=None): @prerequisite([ACCT_PREREQS_MET]) def get_account_details(*, account_id=None): has_error, account, _ = ApiRequest.get('/accounts/%s' % account_id) + if has_error: return account + has_error, stats, _ = ApiRequest.get(f'/accounts/{account_id}/' + 'removal_queue') + + if has_error: + return stats + return _render_with_defaults('account_details.jinja2', CREATE_ACCT=False, - account=account) + account=account, + requested_deletion=stats['status']) @prerequisite([ACCT_PREREQS_MET]) @@ -1316,6 +1324,29 @@ def get_create_nonhuman_source(*, account_id=None): account_id=account_id) +# Note: ideally this would be represented as a DELETE, not as a POST +# However, it is used as a form submission action, and HTML forms do not +# support delete as an action +@prerequisite([ACCT_PREREQS_MET]) +def post_request_account_removal(*, account_id, body): + # PUT is used to add the account_id to the queue + # DELETE is used to remove the account_id from the queue, if it's + # still there. + + user_delete_reason = body.get('user_delete_reason') + + url = f'/accounts/{account_id}/removal_queue' \ + f'?user_delete_reason={user_delete_reason}' + + has_error, put_output, _ = ApiRequest.put(url) + + if has_error: + return put_output + + return _render_with_defaults('request_account_deletion_confirm.jinja2', + account_id=account_id) + + @prerequisite([ACCT_PREREQS_MET]) def post_create_nonhuman_source(*, account_id=None, body=None): has_error, sources_output, _ = ApiRequest.post( @@ -2782,8 +2813,21 @@ def get_interactive_account_search(email_query): if do_return: return email_diagnostics - accounts = [{"email": acct['email'], "account_id": acct['id']} - for acct in email_diagnostics['accounts']] + accounts = [] + for acct in email_diagnostics['accounts']: + if acct['auth_issuer'] is None and acct['auth_sub'] is None: + authrocket_status = "Missing" + elif acct['auth_issuer'] is None or acct['auth_sub'] is None: + authrocket_status = "Faulty - Contact Admin" + else: + authrocket_status = "Authenticated" + acct_diag = { + "email": acct['email'], + "account_id": acct['id'], + "authrocket_status": authrocket_status + } + accounts.append(acct_diag) + return _render_with_defaults('admin_home.jinja2', accounts=accounts) @@ -2797,6 +2841,7 @@ def post_account_delete(body): raise Unauthorized() account_to_delete = body.get('account_id') + delete_reason = body.get('delete_reason') if account_to_delete is None: raise Unauthorized() @@ -2811,8 +2856,10 @@ def post_account_delete(body): if accts_output['account_type'] != 'standard': return get_rootpath() - has_error, delete_output, _ = ApiRequest.delete( - '/accounts/%s' % (account_to_delete,)) + url = f'/admin/account_removal/{account_to_delete}' \ + f'?delete_reason={delete_reason}' + + has_error, delete_output, _ = ApiRequest.delete(url) if has_error: return delete_output @@ -2820,6 +2867,37 @@ def post_account_delete(body): return get_rootpath() +def post_account_ignore_delete(body): + if not session.get(ADMIN_MODE_KEY, False): + raise Unauthorized() + + account_details = session.get(LOGIN_INFO_KEY) + if account_details is None: + raise Unauthorized() + + account_to_ignore = body.get('account_id') + if account_to_ignore is None: + raise Unauthorized() + + # preserve 'standard-accounts-only' logic for now. + # admin accounts shouldn't be requesting their own deletion. + do_return, accts_output, _ = ApiRequest.get( + '/accounts/%s' % (account_to_ignore, )) + if do_return: + return accts_output + + if accts_output['account_type'] != 'standard': + return get_rootpath() + + url = '/admin/account_removal/%s' % account_to_ignore + has_error, ignore_output, _ = ApiRequest.put(url) + + if has_error: + return ignore_output + + return get_rootpath() + + def get_perk_fulfillment_state(): if not session.get(ADMIN_MODE_KEY, False): raise Unauthorized() @@ -3381,6 +3459,26 @@ def post_campaign_edit(body): return get_campaign_edit(campaign_info['campaign_id']) +def get_account_removal_requests(): + if not session.get(ADMIN_MODE_KEY, False): + raise Unauthorized() + + account_details = session.get(LOGIN_INFO_KEY) + if account_details is None: + raise Unauthorized() + + do_return, diagnostics, _ = ApiRequest.get( + "/admin/account_removal/list", + params={} + ) + + if do_return: + return diagnostics + + return _render_with_defaults('admin_requests_account_removal_list.jinja2', + diagnostics=diagnostics) + + def get_submit_interest(campaign_id=None, source=None): valid_campaign = False campaign_info = None diff --git a/microsetta_interface/model_i18n.py b/microsetta_interface/model_i18n.py index aafe6273..556b99fd 100644 --- a/microsetta_interface/model_i18n.py +++ b/microsetta_interface/model_i18n.py @@ -13,8 +13,9 @@ LANGUAGES = { EN_US_KEY: Lang("en_US", "English"), ES_MX_KEY: Lang("es_MX", "Español (México)"), - ES_ES_KEY: Lang("es_ES", "Español (España)"), - JA_JP_KEY: Lang("ja_JP", "日本語") + ES_ES_KEY: Lang("es_ES", "Español (España)") + # In case we want to reactivate Japanese in the future + # JA_JP_KEY: Lang("ja_JP", "日本語") } # We need a default full locale when a user's browser only sends a partial diff --git a/microsetta_interface/routes.yaml b/microsetta_interface/routes.yaml index 589d89dd..75fd7842 100644 --- a/microsetta_interface/routes.yaml +++ b/microsetta_interface/routes.yaml @@ -586,6 +586,32 @@ paths: schema: type: string + # same as above + '/accounts/{account_id}/request/remove': + post: + operationId: microsetta_interface.implementation.post_request_account_removal + tags: + - Account + parameters: + - $ref: '#/components/parameters/account_id' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + user_delete_reason: + type: string + nullable: true + responses: + '200': + description: Remove account request submitted + content: + text/html: + schema: + type: string + + '/accounts/{account_id}/sources/{source_id}/claim_samples': post: operationId: microsetta_interface.implementation.post_claim_samples @@ -1064,6 +1090,9 @@ paths: account_id: type: string nullable: false + delete_reason: + type: string + nullable: true responses: '200': @@ -1073,6 +1102,28 @@ paths: schema: type: string + '/admin/account_ignore_delete': + post: + operationId: microsetta_interface.implementation.post_account_ignore_delete + tags: + - Admin + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + account_id: + type: string + nullable: false + responses: + '200': + description: Account successfully removed from delete queue, redirect to home + content: + text/html: + schema: + type: string + '/admin/perk_fulfillment_state': get: operationId: microsetta_interface.implementation.get_perk_fulfillment_state @@ -1439,6 +1490,19 @@ paths: schema: type: string + '/admin/account_removal/list': + get: + operationId: microsetta_interface.implementation.get_account_removal_requests + tags: + - Admin + responses: + '200': + description: List of account removal requests for admin users to view/edit + content: + text/html: + schema: + type: string + components: parameters: diff --git a/microsetta_interface/templates/account_details.jinja2 b/microsetta_interface/templates/account_details.jinja2 index c4d738b2..503ecf47 100644 --- a/microsetta_interface/templates/account_details.jinja2 +++ b/microsetta_interface/templates/account_details.jinja2 @@ -128,6 +128,17 @@ } },cc); }); + + function verifyDeleteUserRequest(){ + let confirmMsg = "{{ _('You are requesting to delete your account.') }} " + + "{{ _('This operation cannot be undone. Are you sure you want to delete this account?') }} "; + + let reason = prompt("{{ _('Please provide a reason for deletion (Optional):') }}"); + document.getElementById("user_delete_reason").value = reason; + + return window.confirm(confirmMsg); + + } {% endblock %} {% block breadcrumb %} @@ -411,6 +422,7 @@ {% endif %} +

{% if admin_mode %}
@@ -438,8 +450,45 @@ -
{% endif %} + + {% if not admin_mode and not CREATE_ACCT %} + {% if requested_deletion %} +
+
+
+

+

+
+ {{ _('Your account removal request is being reviewed. You will be notified via email once your account has been deleted.') }} +
+
+
+
+ {% else %} +
+
+
+

+

+
+
+ {{ _('If you wish to delete this account, please click the following button to submit your request to an administrator.') }} + +

+ +

+ {{ _('IMPORTANT: Once you click this button, the request cannot be undone. Your account cannot be restored after it has been deleted.') }} +
+
+
+
+
+ {% endif %} + {% endif %} + {% endblock %} diff --git a/microsetta_interface/templates/admin_home.jinja2 b/microsetta_interface/templates/admin_home.jinja2 index 63732742..2fc24326 100644 --- a/microsetta_interface/templates/admin_home.jinja2 +++ b/microsetta_interface/templates/admin_home.jinja2 @@ -13,18 +13,24 @@
{{ _('Account ID') }}
+
+ {{ _('AuthRocket Status') }} +
{% for account in accounts %}
-
+ -
+
{{ account.account_id|e }}
+
+ {{ account.authrocket_status|e }} +
{% endfor %} diff --git a/microsetta_interface/templates/admin_requests_account_removal_list.jinja2 b/microsetta_interface/templates/admin_requests_account_removal_list.jinja2 new file mode 100644 index 00000000..b3a9f030 --- /dev/null +++ b/microsetta_interface/templates/admin_requests_account_removal_list.jinja2 @@ -0,0 +1,97 @@ +{% extends "sitebase.jinja2" %} +{% set page_title = _("Requests for Account Removal") %} +{% set show_breadcrumbs = False %} +{% block content %} + +

{{ _('Requests for Account Removal') }}

+
+ {% if diagnostics is not none and diagnostics|length > 0 %} +
+
+
+ {{ _('ID') }} +
+
+ {{ _('Account ID') }} +
+
+ {{ _('Email') }} +
+
+ {{ _('First Name') }} +
+
+ {{ _('Last Name') }} +
+
+ {{ _('Requested On') }} +
+
+ {{ _('Reason for Deletion') }} +
+
+   +
+
+   +
+
+ {% for row in diagnostics %} +
+
+
+ {{ row.id |e }} +
+
+ {{ row.account_id |e }} +
+
+ {{ row.email |e }} +
+
+ {{ row.first_name |e }} +
+
+ {{ row.last_name |e }} +
+
+ {{ row.requested_on |e }} +
+
+ {{ row.user_delete_reason |e }} +
+
+
+ + + +
+
+
+
+ + + +
+
+ +
+
+ {% endfor %} +
+

+ {% else %} + {{ _('No requests found') }} + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/microsetta_interface/templates/new_results_page.jinja2 b/microsetta_interface/templates/new_results_page.jinja2 index c469b7c4..3654aacd 100644 --- a/microsetta_interface/templates/new_results_page.jinja2 +++ b/microsetta_interface/templates/new_results_page.jinja2 @@ -47,6 +47,11 @@ width: 30%; margin: 20px; } + @media (max-width: 575.98px) { + div.diversity-compare { + width: 90%; + } + } .diversity-category { color: #006a96; } @@ -116,6 +121,11 @@ border-color: #006a96; width: 70%; } + @media (max-width: 575.98px) { + div.how_you_compare_section { + width: 100%; + } + } div.how_you_compare_section h3 { color: #006a96; font-weight: lighter; @@ -147,7 +157,11 @@ width: 35%; box-shadow: 0 4px 8px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%); } - + @media (max-width: 575.98px) { + div.your_sample_diversity_inset { + width: 100%; + } + } .scatter-bg { background-image: url('/static/img/scatter.png'); background: url('/static/img/scatter.png'); @@ -1022,7 +1036,7 @@ } // We override the default alpha_metric from state for this page. - let alpha_metric = "observed_features" + let alpha_metric = "shannon" for (let key in POST_DATA) { $.ajax( { @@ -1034,7 +1048,7 @@ data: JSON.stringify(POST_DATA[key]), contentType: "application/json", success: function (data) { - let display = data["group_summary"]["median"] + let display = Math.round(data["group_summary"]["median"] * 100)/100; $(SELECTORS[key]).text(display).removeClass("spinner-grow spinner-grow-sm"); } }).fail(function (result, textStatus, errorThrown) { @@ -1050,7 +1064,7 @@ method: "GET", contentType: "application/json", success: function (data) { - let display = data["data"] + let display = Math.round(data["data"] * 100)/100; $("#diversity_in_sample").text(display).removeClass("spinner-grow spinner-grow-sm"); } }).fail(function (result, textStatus, errorThrown) { @@ -1185,15 +1199,15 @@ END BLOCK COMMENTED OUT WHILE TAXA VIOLIN IS HIDDEN */ } - function updateObservedOTUs(state) { - var us_text = document.getElementById('average-american-observed-otus'); - var hadza_text = document.getElementById('average-hadza-observed-otus'); + function updateObservedAverages(state) { + var us_text = document.getElementById('average-american-observed-average'); + var hadza_text = document.getElementById('average-hadza-observed-average'); if (state.dataset_type.value === 'WGS') { - us_text.innerText = '107'; - hadza_text.innerText = '129'; + us_text.innerText = '5.31'; + hadza_text.innerText = '6.30'; } else if (state.dataset_type.value === '16S') { - us_text.innerText = '93'; - hadza_text.innerText = '132'; + us_text.innerText = '4.47'; + hadza_text.innerText = '5.20'; } } @@ -1326,7 +1340,7 @@ updateDatasetDetails(state); getMicrobiomeMapsRasterizedPlots(state); - updateObservedOTUs(state); + updateObservedAverages(state); }, state.dataset_input); refreshDatasets(state); @@ -1376,7 +1390,7 @@

{{ _('Diversity') }}

- {{ _('How many kinds of microbes were in your sample? Check out your') }} {{ _('Diversity') }}. + {{ _('How diverse is your microbiome? Check out your') }} {{ _('Diversity') }}.


@@ -1410,16 +1424,28 @@

{{ _('Diversity') }}


- {{ _('Recent estimates suggest hundreds of billions of types of microbes live on earth. This number dwarfs the meager 2 million different kinds of animals and plants discovered on the planet.') }} + {{ _('This section provides insights into your microbiome diversity and how it compares to others\'.') }} +

+

+ {{ _('How do we calculate your diversity value?') }}

- {{ _('If we were to randomly select a set number of microbial DNA sequences from a stool sample, we would find that the average American has FILLIN different types of microbes, which is lower than we find in people living a more hunter-gatherer lifestyle, like the FILLIN we find in samples from the Hadza people of Tanzania.') }} + {{ _('Diversity can be measured in a lot of different ways. We measure it by first taking the DNA sequences from your sample and figuring out which Bacteria and Archaea they may have come from. This produces a good estimate of how many of each kind of microbe you have. The measure we use, called Shannon\'s index, takes into account how many different kinds of microbes make up your gut community (also known as richness) and how relatively abundant each of those microbes are (also known as evenness).') }}

- {{ _('The number of microbes found in your sample can be very close or vastly different from this average, and it doesn\'t necessarily mean something is right or wrong with you. We are all different, and this number is affected by many different factors.') }} + {{ _('This number isn\'t exact for many reasons. However, because it\'s calculated in the same way for every sample in our dataset, it provides insight into how your microbial diversity compares to others\'. For example, the average diversity value for people living in the US in our dataset is FILLIN, which is lower than we find in people living a more hunter-gatherer lifestyle like the Hadza people of Tanzania, who have an average value of FILLIN. ') }} +

+

+ {{ _('What does this number mean for my health?') }} +

+

+ {{ _('This number reflects the variety of Bacteria and Archaea observed in your fecal sample at the time of testing. Currently, there\'s no standard for how gut microbiome diversity relates to health. While some studies suggest a potential link between higher diversity and specific health benefits, that is not true universally and more research is needed to understand the relationship. For example, it may be that the specific microbes present are more important than having many different types of microbes.') }} +

+

+ {{ _('It\'s important to remember that everyone\'s gut microbiome is unique and changes over time. If your score differs from the average, it doesn\'t necessarily indicate a health concern.') }}

-
{{ _('Number of different microbes found in your sample:') }}
+
{{ _('Your sample\'s diversity value:') }}
...
@@ -1437,7 +1463,7 @@ {{ _('Participants who eat more than 30 plants per week') }}
- {{ _('Average number of different microbes:') }} ... + {{ _('Average diversity value:') }} ...
@@ -1448,7 +1474,7 @@ {{ _('Participants who exercise regularly') }}
- {{ _('Average number of different microbes:') }} ... + {{ _('Average diversity value:') }} ...
@@ -1461,7 +1487,7 @@ {{ _('Participants who drink 1L of water regularly') }}
- {{ _('Average number of different microbes:') }} ... + {{ _('Average diversity value:') }} ...
@@ -1472,7 +1498,7 @@ {{ _('Participants who sleep more than 6 hours per night') }}
- {{ _('Average number of different microbes:') }} ... + {{ _('Average diversity value:') }} ...
@@ -1607,10 +1633,6 @@ {{ _('To learn more about what the taxonomic ranks (column headers) mean, place your cursor over their names. Additionally, pronunciation can be a challenge considering the language of origin varies for the names between Latin, Greek, Norse, Chinese, and more. A pronunciation guide can be found here.') }}

-

- {{ _('The number of microbes listed here might be different from what was reported as your Diversity because of the way microbes are grouped according to their taxonomy (i.e. names). There are likely many species of microbes in your sample that are all grouped in the same genus.') }} -

-
diff --git a/microsetta_interface/templates/request_account_deletion_confirm.jinja2 b/microsetta_interface/templates/request_account_deletion_confirm.jinja2 new file mode 100644 index 00000000..4f57bf0a --- /dev/null +++ b/microsetta_interface/templates/request_account_deletion_confirm.jinja2 @@ -0,0 +1,36 @@ +{% extends "sitebase.jinja2" %} +{% set page_title = _("Account Deletion Request") %} +{% set show_breadcrumbs = True %} + +{% block head %} + + + + + + + + +{% endblock %} + +{% block breadcrumb %} + + +{% endblock %} + +{% block content %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/microsetta_interface/templates/sitebase.jinja2 b/microsetta_interface/templates/sitebase.jinja2 index 0193b5cd..8c683f8c 100644 --- a/microsetta_interface/templates/sitebase.jinja2 +++ b/microsetta_interface/templates/sitebase.jinja2 @@ -118,20 +118,23 @@ {% if admin_mode %} -