Merge pull request #17142 from edx/LEARNER-3529/manage_user
Add Manage User Feature to Support Tool
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