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}' + }); + + + +<%block name="pagetitle"> +${_("Manage User")} + + +<%block name="content"> +
+

${_("Student Support: Manage User")}

+
+
+