diff --git a/lms/djangoapps/support/static/support/js/manage_user_factory.js b/lms/djangoapps/support/static/support/js/manage_user_factory.js
new file mode 100644
index 0000000000..6e5e953984
--- /dev/null
+++ b/lms/djangoapps/support/static/support/js/manage_user_factory.js
@@ -0,0 +1,13 @@
+(function(define) {
+ 'use strict';
+
+ define([
+ 'underscore',
+ 'support/js/views/manage_user'
+ ], function(_, ManageUserView) {
+ return function(options) {
+ var params = _.extend({el: '.manage-user-content'}, options);
+ return new ManageUserView(params).render();
+ };
+ });
+}).call(this, define || RequireJS.define);
diff --git a/lms/djangoapps/support/static/support/js/models/manage_user.js b/lms/djangoapps/support/static/support/js/models/manage_user.js
new file mode 100644
index 0000000000..0b6a26962e
--- /dev/null
+++ b/lms/djangoapps/support/static/support/js/models/manage_user.js
@@ -0,0 +1,30 @@
+(function(define) {
+ 'use strict';
+ define(['backbone', 'underscore'], function(Backbone, _) {
+ return Backbone.Model.extend({
+
+ initialize: function(options) {
+ this.user = options.user || '';
+ this.baseUrl = options.baseUrl;
+ },
+
+ url: function() {
+ return this.baseUrl + this.user;
+ },
+ disableAccount: function() {
+ return $.ajax({
+ url: this.url(),
+ type: 'POST',
+ contentType: 'application/json',
+ data: JSON.stringify({
+ username_or_email: this.get('username')
+ }),
+ success: _.bind(function(response) {
+ this.set('response', response.success_msg);
+ this.set('status', response.status);
+ }, this)
+ });
+ }
+ });
+ });
+}).call(this, define || RequireJS.define);
diff --git a/lms/djangoapps/support/static/support/js/views/manage_user.js b/lms/djangoapps/support/static/support/js/views/manage_user.js
new file mode 100644
index 0000000000..c4d5168292
--- /dev/null
+++ b/lms/djangoapps/support/static/support/js/views/manage_user.js
@@ -0,0 +1,82 @@
+(function(define) {
+ 'use strict';
+ define([
+ 'backbone',
+ 'underscore',
+ 'moment',
+ 'edx-ui-toolkit/js/utils/html-utils',
+ 'support/js/models/manage_user',
+ 'text!support/templates/manage_user.underscore'
+ ], function(Backbone, _, moment, HtmlUtils, ManageUserModel, manageUserTemplate) {
+ return Backbone.View.extend({
+ manageUserTpl: HtmlUtils.template(manageUserTemplate),
+
+ events: {
+ 'submit .manage-user-form': 'search',
+ 'click .disable-account-btn': 'disableAccount'
+ },
+ initialize: function(options) {
+ var user = options.user;
+ this.initialUser = user;
+ this.userSupportUrl = options.userSupportUrl;
+ this.user_profile = new ManageUserModel({
+ user: user,
+ baseUrl: options.userDetailUrl
+ });
+ this.user_profile.on('change', _.bind(this.render, this));
+ },
+ render: function() {
+ var user = this.user_profile.user;
+ HtmlUtils.setHtml(this.$el, this.manageUserTpl({
+ user: user,
+ user_profile: this.user_profile,
+ formatDate: function(date) {
+ return date ? moment.utc(date).format('lll z') : 'N/A';
+ }
+ }));
+
+ this.checkInitialSearch();
+ return this;
+ },
+
+ /*
+ * Check if the URL has provided an initial search, and
+ * perform that search if so.
+ */
+ checkInitialSearch: function() {
+ if (this.initialUser) {
+ delete this.initialUser;
+ this.$('.manage-user-form').submit();
+ }
+ },
+
+ /*
+ * Return the user's search string.
+ */
+ getSearchString: function() {
+ return this.$('#manage-user-query-input').val();
+ },
+
+ /*
+ * Perform the search. Renders the view on success.
+ */
+ search: function(event) {
+ event.preventDefault();
+ this.user_profile.user = this.getSearchString();
+ this.user_profile.fetch({
+ success: _.bind(function() {
+ this.user_profile.set('response', '');
+ this.user_profile.set(
+ 'date_joined',
+ moment(this.user_profile.get('date_joined')).format('YYYY-MM-DD')
+ );
+ this.render();
+ }, this)
+ });
+ },
+ disableAccount: function() {
+ this.user_profile.disableAccount();
+ }
+ });
+ });
+}).call(this, define || RequireJS.define);
diff --git a/lms/djangoapps/support/static/support/templates/manage_user.underscore b/lms/djangoapps/support/static/support/templates/manage_user.underscore
new file mode 100644
index 0000000000..96027d11fd
--- /dev/null
+++ b/lms/djangoapps/support/static/support/templates/manage_user.underscore
@@ -0,0 +1,46 @@
+
+
+
+
+<% if (user_profile.get('username') !== undefined) { %>
+
+
+
+
+ | <%- gettext('Username') %> |
+ <%- gettext('Email') %> |
+ <%- gettext('Date Joined') %> |
+ <%- gettext('Password Status') %> |
+ <%- gettext('Action') %> |
+
+
+
+
+ | <% print(user_profile.get('username')) %> |
+ <% print(user_profile.get('email')) %> |
+ <% print(user_profile.get('date_joined')) %> |
+ <% print(user_profile.get('status')) %> |
+
+
+ |
+
+
+
+
+<% } %>
+
+<% if (user_profile.get('response')) {%>
+ <% print(user_profile.get('response')) %>
+<% } %>
diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py
index 70715ab341..05cf547969 100644
--- a/lms/djangoapps/support/tests/test_views.py
+++ b/lms/djangoapps/support/tests/test_views.py
@@ -10,6 +10,7 @@ from datetime import datetime, timedelta
import ddt
import pytest
+from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db.models import signals
from nose.plugins.attrib import attr
@@ -44,6 +45,51 @@ class SupportViewTestCase(ModuleStoreTestCase):
self.assertTrue(success, msg="Could not log in")
+class SupportViewManageUserTests(SupportViewTestCase):
+ """
+ Base class for support view tests.
+ """
+
+ def setUp(self):
+ """Make the user support staff"""
+ super(SupportViewManageUserTests, self).setUp()
+ SupportStaffRole().add_users(self.user)
+
+ def test_get_support_form(self):
+ """
+ Tests Support View to return Manage User Form
+ """
+ url = reverse('support:manage_user')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ def test_get_form_with_user_info(self):
+ """
+ Tests Support View to return Manage User Form
+ with user info
+ """
+ url = reverse('support:manage_user_detail') + self.user.username
+ response = self.client.get(url)
+ data = json.loads(response.content)
+ self.assertEqual(data['username'], self.user.username)
+
+ def test_disable_user_account(self):
+ """
+ Tests Support View to disable the user account
+ """
+ test_user = UserFactory(
+ username='foobar', email='foobar@foobar.com', password='foobar'
+ )
+ url = reverse('support:manage_user_detail') + test_user.username
+ response = self.client.post(url, data={
+ 'username_or_email': test_user.username
+ })
+ data = json.loads(response.content)
+ self.assertEqual(data['success_msg'], 'User Disabled Successfully')
+ test_user = User.objects.get(username=test_user.username, email=test_user.email)
+ self.assertEqual(test_user.has_usable_password(), False)
+
+
@attr(shard=3)
@ddt.ddt
class SupportViewAccessTests(SupportViewTestCase):
@@ -59,7 +105,9 @@ class SupportViewAccessTests(SupportViewTestCase):
'support:certificates',
'support:refund',
'support:enrollment',
- 'support:enrollment_list'
+ 'support:enrollment_list',
+ 'support:manage_user',
+ 'support:manage_user_detail'
), (
(GlobalStaff, True),
(SupportStaffRole, True),
@@ -85,7 +133,9 @@ class SupportViewAccessTests(SupportViewTestCase):
"support:certificates",
"support:refund",
"support:enrollment",
- "support:enrollment_list"
+ "support:enrollment_list",
+ "support:manage_user",
+ "support:manage_user_detail"
)
def test_require_login(self, url_name):
url = reverse(url_name)
diff --git a/lms/djangoapps/support/urls.py b/lms/djangoapps/support/urls.py
index 2334dd3163..5c5019984e 100644
--- a/lms/djangoapps/support/urls.py
+++ b/lms/djangoapps/support/urls.py
@@ -18,4 +18,10 @@ urlpatterns = [
views.EnrollmentSupportListView.as_view(),
name="enrollment_list"
),
+ url(r'^manage_user/?$', views.ManageUserSupportView.as_view(), name="manage_user"),
+ url(
+ r'^manage_user/(?P[\w.@+-]+)?$',
+ views.ManageUserDetailView.as_view(),
+ name="manage_user_detail"
+ ),
]
diff --git a/lms/djangoapps/support/views/__init__.py b/lms/djangoapps/support/views/__init__.py
index fa1d3c46a9..55db08fce7 100644
--- a/lms/djangoapps/support/views/__init__.py
+++ b/lms/djangoapps/support/views/__init__.py
@@ -6,3 +6,4 @@ from .index import *
from .certificate import *
from .enrollments import *
from .refund import *
+from .manage_user import *
diff --git a/lms/djangoapps/support/views/index.py b/lms/djangoapps/support/views/index.py
index 5085554d25..e1ccb0ebb6 100644
--- a/lms/djangoapps/support/views/index.py
+++ b/lms/djangoapps/support/views/index.py
@@ -26,6 +26,11 @@ SUPPORT_INDEX_URLS = [
"name": _("Enrollment"),
"description": _("View and update learner enrollments."),
},
+ {
+ "url": reverse_lazy("support:manage_user"),
+ "name": _("Manage User"),
+ "description": _("Disable User Account"),
+ },
]
diff --git a/lms/djangoapps/support/views/manage_user.py b/lms/djangoapps/support/views/manage_user.py
new file mode 100644
index 0000000000..5d08260c19
--- /dev/null
+++ b/lms/djangoapps/support/views/manage_user.py
@@ -0,0 +1,66 @@
+"""
+Support tool for disabling user accounts.
+"""
+from django.contrib.auth import get_user_model
+from django.core.urlresolvers import reverse
+from django.db.models import Q
+from django.utils.decorators import method_decorator
+from django.utils.translation import ugettext as _
+from django.views.generic import View
+from rest_framework.generics import GenericAPIView
+from rest_framework.response import Response
+
+from edxmako.shortcuts import render_to_response
+from lms.djangoapps.support.decorators import require_support_permission
+from openedx.core.djangoapps.user_api.accounts.serializers import AccountUserSerializer
+from util.json_request import JsonResponse
+
+
+class ManageUserSupportView(View):
+ """
+ View for viewing and managing user accounts, used by the
+ support team.
+ """
+
+ @method_decorator(require_support_permission)
+ def get(self, request):
+ """Render the manage user support tool view."""
+ return render_to_response('support/manage_user.html', {
+ _('username'): request.GET.get('user', ''),
+ _('user_support_url'): reverse('support:manage_user'),
+ _('user_detail_url'): reverse('support:manage_user_detail')
+ })
+
+
+class ManageUserDetailView(GenericAPIView):
+ """
+ Allows viewing and disabling learner accounts by support
+ staff.
+ """
+
+ @method_decorator(require_support_permission)
+ def get(self, request, username_or_email):
+ """
+ Returns details for the given user, along with
+ information about its username and joining date.
+ """
+ try:
+ user = get_user_model().objects.get(
+ Q(username=username_or_email) | Q(email=username_or_email)
+ )
+ data = AccountUserSerializer(user, context={'request': request}).data
+ data['status'] = _('Usable') if user.has_usable_password() else _('Unusable')
+ return JsonResponse(data)
+ except get_user_model().DoesNotExist:
+ return JsonResponse([])
+
+ @method_decorator(require_support_permission)
+ def post(self, request, username_or_email):
+ """Allows support staff to disable a user's account."""
+ user = get_user_model().objects.get(
+ Q(username=username_or_email) | Q(email=username_or_email)
+ )
+ user.set_unusable_password()
+ user.save()
+ password_status = _('Usable') if user.has_usable_password() else _('Unusable')
+ return JsonResponse({'success_msg': _('User Disabled Successfully'), 'status': password_status})
diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js
index b994a38848..20765e64dd 100644
--- a/lms/static/lms/js/build.js
+++ b/lms/static/lms/js/build.js
@@ -45,6 +45,7 @@
'lms/js/preview/preview_factory',
'support/js/certificates_factory',
'support/js/enrollment_factory',
+ 'support/js/manage_user_factory',
'teams/js/teams_tab_factory',
'js/dateutil_factory'
]),
diff --git a/lms/static/sass/views/_support.scss b/lms/static/sass/views/_support.scss
index 22c9bfa62c..b64f6e3c6d 100644
--- a/lms/static/sass/views/_support.scss
+++ b/lms/static/sass/views/_support.scss
@@ -16,6 +16,13 @@
}
}
+.manage-user-search{
+ margin: 40px 0;
+
+ input[name="query"] {
+ width: 350px;
+ }
+}
.certificates-results {
table {
@@ -146,6 +153,37 @@
}
}
+.manage-user-results {
+
+ .manage-user-table {
+ display: inline-block;
+ }
+
+ th {
+ @extend %t-title7;
+ text-align: center;
+ }
+
+ td{
+ padding: 0 23px;
+ }
+
+ .disable-account-btn,
+ .disable-account-btn:hover {
+ @extend %t-action4;
+ letter-spacing: normal;
+ text-transform: none;
+ background-image: none;
+ border: none;
+ box-shadow: none;
+ text-shadow: none;
+ }
+}
+
+.manage-user-content{
+ text-align: center;
+}
+
.contact-us-wrapper {
min-width: auto;
diff --git a/lms/templates/support/manage_user.html b/lms/templates/support/manage_user.html
new file mode 100644
index 0000000000..61ef5262a2
--- /dev/null
+++ b/lms/templates/support/manage_user.html
@@ -0,0 +1,31 @@
+<%page expression_filter="h"/>
+
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.js_utils import js_escaped_string
+%>
+
+<%namespace name='static' file='../static_content.html'/>
+
+<%inherit file="../main.html" />
+
+<%block name="js_extra">
+<%static:require_module module_name="support/js/manage_user_factory" class_name="ManageUserFactory">
+ new ManageUserFactory({
+ user: '${username | n, js_escaped_string}',
+ userSupportUrl: '${user_support_url | n, js_escaped_string}',
+ userDetailUrl: '${user_detail_url | n, js_escaped_string}'
+ });
+%static:require_module>
+%block>
+
+<%block name="pagetitle">
+${_("Manage User")}
+%block>
+
+<%block name="content">
+
+ ${_("Student Support: Manage User")}
+
+
+%block>