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 %}
+
+
+
+{% 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