From 266c8fe7373a54d3b8010d03c043f5ae1396ac04 Mon Sep 17 00:00:00 2001 From: uzairr Date: Wed, 10 Jan 2018 03:05:27 +0500 Subject: [PATCH] Add Manage User Feature to Support Tool Support Team receive much requests from the users to disable their accounts on edX.To fullfill their requests, Support has to contact DevOps.To avoid it, a new feature is added which can easily disable the user account. LEARNER-3529 --- .../static/support/js/manage_user_factory.js | 13 +++ .../static/support/js/models/manage_user.js | 30 +++++++ .../static/support/js/views/manage_user.js | 82 +++++++++++++++++++ .../support/templates/manage_user.underscore | 46 +++++++++++ lms/djangoapps/support/tests/test_views.py | 54 +++++++++++- lms/djangoapps/support/urls.py | 6 ++ lms/djangoapps/support/views/__init__.py | 1 + lms/djangoapps/support/views/index.py | 5 ++ lms/djangoapps/support/views/manage_user.py | 66 +++++++++++++++ lms/static/lms/js/build.js | 1 + lms/static/sass/views/_support.scss | 38 +++++++++ lms/templates/support/manage_user.html | 31 +++++++ 12 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 lms/djangoapps/support/static/support/js/manage_user_factory.js create mode 100644 lms/djangoapps/support/static/support/js/models/manage_user.js create mode 100644 lms/djangoapps/support/static/support/js/views/manage_user.js create mode 100644 lms/djangoapps/support/static/support/templates/manage_user.underscore create mode 100644 lms/djangoapps/support/views/manage_user.py create mode 100644 lms/templates/support/manage_user.html 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")}

+
+
+