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
This commit is contained in:
uzairr
2018-01-10 03:05:27 +05:00
parent 72d300b822
commit 266c8fe737
12 changed files with 371 additions and 2 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -0,0 +1,46 @@
<div class="manage-user-search">
<form class="manage-user-form">
<label class="sr" for="manage-user-query-input"><%- gettext('Search') %></label>
<input
id="manage-user-query-input"
type="text"
name="query"
value="<%- user %>"
placeholder="<%- gettext('Username') %>">
</input>
<input type="submit" value="<%- gettext('Search') %>" class="btn-disable-on-submit"></input>
</form>
</div>
<% if (user_profile.get('username') !== undefined) { %>
<div class="manage-user-results">
<table id="manage-user-table" class="manage-user-table display compact nowrap">
<thead>
<tr>
<th><%- gettext('Username') %></th>
<th><%- gettext('Email') %></th>
<th><%- gettext('Date Joined') %></th>
<th><%- gettext('Password Status') %></th>
<th><%- gettext('Action') %></th>
</tr>
</thead>
<tbody>
<tr>
<td><% print(user_profile.get('username')) %></td>
<td><% print(user_profile.get('email')) %></td>
<td><% print(user_profile.get('date_joined')) %></td>
<td><% print(user_profile.get('status')) %></td>
<td>
<button class="disable-account-btn">
<%- gettext('Disable Account') %>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<% } %>
<% if (user_profile.get('response')) {%>
<span><% print(user_profile.get('response')) %></span>
<% } %>

View File

@@ -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)

View File

@@ -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<username_or_email>[\w.@+-]+)?$',
views.ManageUserDetailView.as_view(),
name="manage_user_detail"
),
]

View File

@@ -6,3 +6,4 @@ from .index import *
from .certificate import *
from .enrollments import *
from .refund import *
from .manage_user import *

View File

@@ -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"),
},
]

View File

@@ -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})

View File

@@ -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'
]),

View File

@@ -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;

View File

@@ -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">
<section class="container outside-app">
<h1>${_("Student Support: Manage User")}</h1>
<div class="manage-user-content"></div>
</section>
</%block>