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

Account deletion request final #314

Merged
merged 10 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 82 additions & 3 deletions microsetta_interface/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -1316,6 +1324,27 @@ 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
def post_request_account_removal(*, account_id, body):
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved
# 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')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ayobi I'm running into an odd issue with this on the staging server. Private API receives and successfully logs the removal request, but when the template contents are returned, they're enclosed in double quotes and the browser isn't rendering it correctly (screenshot below). Does this match what's happening in your local environment? I'm trying to determine if this is a code issue with the template/yaml file or what's going on here.

request_removal_raw



@prerequisite([ACCT_PREREQS_MET])
def post_create_nonhuman_source(*, account_id=None, body=None):
has_error, sources_output, _ = ApiRequest.post(
Expand Down Expand Up @@ -2797,6 +2826,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()

Expand All @@ -2811,15 +2841,48 @@ 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

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()
Expand Down Expand Up @@ -3381,6 +3444,22 @@ 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()

ayobi marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
65 changes: 65 additions & 0 deletions microsetta_interface/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,33 @@ paths:
schema:
type: string

# same as above
# TODO: Do we need more response codes appended?
ayobi marked this conversation as resolved.
Show resolved Hide resolved
'/accounts/{account_id}/request/remove':
post:
operationId: microsetta_interface.implementation.post_request_account_removal
tags:
- Account
parameters:
- $ref: '#/components/parameters/account_id'
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
user_delete_reason:
type: string
nullable: true
responses:
'200':
description: Display of revised info or error info
content:
text/html:
schema:
type: string

'/accounts/{account_id}/sources/{source_id}/claim_samples':
post:
operationId: microsetta_interface.implementation.post_claim_samples
Expand Down Expand Up @@ -1064,6 +1091,9 @@ paths:
account_id:
type: string
nullable: false
delete_reason:
type: string
nullable: true

responses:
'200':
Expand All @@ -1073,6 +1103,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
Expand Down Expand Up @@ -1439,6 +1491,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:
Expand Down
47 changes: 47 additions & 0 deletions microsetta_interface/templates/account_details.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
</script>
{% endblock %}
{% block breadcrumb %}
Expand Down Expand Up @@ -442,4 +453,40 @@
{% endif %}
</div>
</div>
{% if not admin_mode %}
{% if requested_deletion %}
<div class="container">
<div class="row">
<div class="col-12 {% if samples|length > 0 %}tooltipper{% endif %}" data-title="{{ _('Request account deletion.') }}">
<p>
</p>
<center>
_("Your account removal request is being reviewed. You will be notified via email once your account has been deleted.")
</center>
</div>
</div>
</div>
{% else %}
<div class="container">
ayobi marked this conversation as resolved.
Show resolved Hide resolved
<div class="row">
<div class="col-12 {% if samples|length > 0 %}tooltipper{% endif %}" data-title="{{ _('Request account deletion.') }}">
<p>
</p>
<center>
<form name="request_delete_{{account.account_id}}_form"
method="post" action="/accounts/{{ account.account_id }}/request/remove"
onsubmit="return verifyDeleteUserRequest();">
{{ _('If you wish to delete this account, please click the following button to submit your request to an administrator.') }}
<input type="hidden" id="user_delete_reason" name="user_delete_reason" value="{{ account.user_delete_reason}}">
ayobi marked this conversation as resolved.
Show resolved Hide resolved
<button type="submit" class="btn btn-outline-danger">{{ _('Request Account Deletion') }}</button>
<br>
{{ _('IMPORTANT: Once you click this button, the request cannot be undone. Your account cannot be restored after it has been deleted.') }}
</br>
</form>
</center>
</div>
</div>
</div>
{% endif %}
{% endif %}
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{% extends "sitebase.jinja2" %}
{% set page_title = _("Requests for Account Removal") %}
{% set show_breadcrumbs = False %}
{% block content %}
<script>
function verifyDeleteRequest() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems nearly duplicated in the prior template, is it possible to centralize the method?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe there's something I'm misunderstanding

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah they're pretty similar, for the admin one the prompt wording is slightly different as well as the getElementById

let confirmMsg = "{{ _('Are you sure you want to delete this account?') }}";
let reason = prompt("{{ _('Please provide a reason for deletion:') }}");
if (reason === null) {
return false;
}
document.getElementById("delete_reason").value = reason;
return window.confirm(confirmMsg);
}
</script>
<h4>{{ _('Requests for Account Removal') }}</h4>
<div class="container">
{% if diagnostics is not none and diagnostics|length > 0 %}
<div class="list-group">
<div class="row">
<div class="col-sm" style="text-align: center">
<i>{{ _('ID') }}</i>
</div>
<div class="col-sm" style="text-align: center">
<i>{{ _('Account ID') }}</i>
</div>
<div class="col-md-3" style="text-align: center">
<i>{{ _('Email') }}</i>
</div>
<div class="col-sm" style="text-align: center">
<i>{{ _('First Name') }}</i>
</div>
<div class="col-sm" style="text-align: center">
<i>{{ _('Last Name') }}</i>
</div>
<div class="col-sm" style="text-align: center">
<i>{{ _('Requested On') }}</i>
</div>
<div class="col-sm" style="text-align: center">
<i>{{ _('Reason for Deletion') }}</i>
</div>
<div class="col-sm" style="text-align: center">
&nbsp;
</div>
<div class="col-sm" style="text-align: center">
&nbsp;
</div>
</div>
{% for row in diagnostics %}
<div class="container list-group-item {{loop.cycle('odd', 'even') }}">
<div class="row">
<div class="col-sm" style="text-align: center">
{{ row.id |e }}
</div>
<div class="col-sm" style="text-align: center">
{{ row.account_id |e }}
</div>
<div class="col-md-3" style="text-align: center">
{{ row.email |e }}
</div>
<div class="col-sm" style="text-align: center">
{{ row.first_name |e }}
</div>
<div class="col-sm" style="text-align: center">
{{ row.last_name |e }}
</div>
<div class="col-sm" style="text-align: center">
{{ row.requested_on |e }}
</div>
<div class="col-sm" style="text-align: center">
{{ row.user_delete_reason |e }}
</div>
<div class="col-sm" style="text-align: right">
<form action="/admin/account_delete" method="post" onsubmit="return verifyDeleteRequest();">
<input type="hidden" id="btnDelete" name="account_id" value="{{ row.account_id }}"/>
<input type="hidden" id="delete_reason" name="delete_reason" value="{{ row.delete_reason}}">
<button type="submit" class="btn btn-danger">{{ _('Delete') }}</button>
</form>
</div>
<div class="col-sm" style="text-align: left">
<form action="/admin/account_ignore_delete" method="post">
<input type="hidden" id="btnIgnore" name="account_id" value="{{ row.account_id }}"/>
<input type="hidden" id="delete_reason" name="delete_reason" value="{{ row.delete_reason}}">
<button type="submit" class="btn btn-danger">{{ _('Ignore') }}</button>
</form>
</div>

</div>
</div>
{% endfor %}
</div>
<br/><br/>
{% else %}
{{ _('No requests found') }}
{% endif %}
</div>
{% endblock %}
Loading
Loading