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:
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
<% } %>
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,3 +6,4 @@ from .index import *
|
||||
from .certificate import *
|
||||
from .enrollments import *
|
||||
from .refund import *
|
||||
from .manage_user import *
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
66
lms/djangoapps/support/views/manage_user.py
Normal file
66
lms/djangoapps/support/views/manage_user.py
Normal 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})
|
||||
@@ -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'
|
||||
]),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
31
lms/templates/support/manage_user.html
Normal file
31
lms/templates/support/manage_user.html
Normal 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>
|
||||
Reference in New Issue
Block a user