diff --git a/lms/templates/admin/user_api/accounts/cancel_retirement_action.html b/lms/templates/admin/user_api/accounts/cancel_retirement_action.html new file mode 100644 index 0000000000..e90a6dc555 --- /dev/null +++ b/lms/templates/admin/user_api/accounts/cancel_retirement_action.html @@ -0,0 +1,42 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_static admin_modify %} + +{% block content %} + +
+ +
+ {% csrf_token %} + + {% if form.non_field_errors|length > 0 %} +

+ {% trans "Please correct the errors below." %} +

+ {{ form.non_field_errors }} + {% endif %} + + {% blocktrans with username=retirement.user.username %}Are you sure you want to cancel retirement for user "{{ username }}"? {% endblocktrans %} + +
+ {% for field in form %} +
+ {{ field.errors }} + {{ field.label_tag }} + {{ field }} + {% if field.field.help_text %} +

+ {{ field.field.help_text|safe }} +

+ {% endif %} +
+ {% endfor %} +
+ +
+ +
+ +
+
+ +{% endblock %} diff --git a/openedx/core/djangoapps/user_api/accounts/forms.py b/openedx/core/djangoapps/user_api/accounts/forms.py new file mode 100644 index 0000000000..8dae89caf4 --- /dev/null +++ b/openedx/core/djangoapps/user_api/accounts/forms.py @@ -0,0 +1,40 @@ +""" +Django forms for accounts +""" + +from django import forms +from django.core.exceptions import ValidationError + + +class RetirementQueueDeletionForm(forms.Form): + """ + Admin form to facilitate learner retirement cancellation + """ + cancel_retirement = forms.BooleanField(required=True) + + def save(self, retirement): + """ + When the form is POSTed we double-check the retirment status + and perform the necessary steps to cancel the retirement + request. + """ + if retirement.current_state.state_name != 'PENDING': + self.add_error( + None, + # Translators: 'current_state' is a string from an enumerated list indicating the learner's retirement + # state. Example: FORUMS_COMPLETE + "Retirement requests can only be cancelled for users in the PENDING state." + " Current request state for '{original_username}': {current_state}".format( + original_username=retirement.original_username, + current_state=retirement.current_state.state_name + ) + ) + raise ValidationError('Retirement is in the wrong state!') + + # Load the user record using the retired email address -and- change the email address back. + retirement.user.email = retirement.original_email + retirement.user.save() + + # Delete the user retirement status record. + # No need to delete the accompanying "permanent" retirement request record - it gets done via Django signal. + retirement.delete() diff --git a/openedx/core/djangoapps/user_api/admin.py b/openedx/core/djangoapps/user_api/admin.py index 75002f5aec..0c9d5512ca 100644 --- a/openedx/core/djangoapps/user_api/admin.py +++ b/openedx/core/djangoapps/user_api/admin.py @@ -1,8 +1,16 @@ """ Django admin configuration pages for the user_api app """ -from django.contrib import admin +from django.conf.urls import url +from django.contrib import admin, messages +from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse +from django.http import HttpResponseForbidden, HttpResponseRedirect +from django.template.response import TemplateResponse +from django.utils.html import format_html +from django.utils.translation import ugettext as _ +from openedx.core.djangoapps.user_api.accounts.forms import RetirementQueueDeletionForm from .models import UserRetirementPartnerReportingStatus, RetirementState, UserRetirementStatus, UserRetirementRequest @@ -24,23 +32,108 @@ class UserRetirementStatusAdmin(admin.ModelAdmin): """ Admin interface for the UserRetirementStatus model. """ - list_display = ('user', 'original_username', 'current_state', 'modified') + list_display = ('user', 'original_username', 'current_state', 'modified', 'retirement_actions') list_filter = ('current_state',) raw_id_fields = ('user',) search_fields = ('original_username', 'retired_username', 'original_email', 'retired_email', 'original_name') + def cancel_retirement(self, request, retirement_id): + """ + Executed when the admin clicks the "Cancel" button on a UserRetirementStatus row, + this handles the confirmation view form, top level error handling, and permissions. + """ + if not request.user.has_perm('user_api.change_userretirementstatus'): + return HttpResponseForbidden(_("Permission Denied")) + + retirement = self.get_object(request, retirement_id) + + redirect_url = reverse( + 'admin:user_api_userretirementstatus_changelist', + current_app=self.admin_site.name, + ) + + if retirement is None: + self.message_user(request, _('Retirement does not exist!'), level=messages.ERROR) + return HttpResponseRedirect(redirect_url) + + if request.method != 'POST': + form = RetirementQueueDeletionForm() + else: + form = RetirementQueueDeletionForm(request.POST) + if form.is_valid(): + try: + form.save(retirement) + self.message_user(request, _('Success')) + return HttpResponseRedirect(redirect_url) + except ValidationError: + # An exception in form.save will display errors on the form page + pass + + context = self.admin_site.each_context(request) + context['opts'] = self.model._meta + context['form'] = form + context['retirement'] = retirement + + return TemplateResponse( + request, + 'admin/user_api/accounts/cancel_retirement_action.html', + context, + ) + + def get_urls(self): + """ + Adds our custom URL to the admin + """ + urls = super(UserRetirementStatusAdmin, self).get_urls() + custom_urls = [ + url( + r'^(?P.+)/cancel_retirement/$', + self.admin_site.admin_view(self.cancel_retirement), + name='cancel-retirement', + ), + ] + return custom_urls + urls + + def retirement_actions(self, obj): + """ + Creates the HTML button in the admin for cancelling retirements, + but only if the row is in the right state. + """ + try: + if obj.current_state.state_name == 'PENDING': + return format_html( + '{} ', + reverse('admin:cancel-retirement', args=[obj.pk]), + _('Cancel') + ) + return format_html('') + except RetirementState.DoesNotExist: + # If the states don't exist, nothing to do here + return format_html('') + + retirement_actions.short_description = _('Actions') + retirement_actions.allow_tags = True + def get_actions(self, request): + """ + Removes the default bulk delete option provided by Django, + it doesn't do what we need for this model. + """ actions = super(UserRetirementStatusAdmin, self).get_actions(request) if 'delete_selected' in actions: del actions['delete_selected'] return actions - # Remove "add" button from admin def has_add_permission(self, request): + """ + Removes the "add" button from admin + """ return False - # Remove "delete" button from admin def has_delete_permission(self, request, obj=None): + """ + Removes the "delete" button from admin + """ return False class Meta(object):