Merge pull request #17142 from edx/LEARNER-3529/manage_user

Add Manage User Feature to Support Tool
This commit is contained in:
Uzair Rasheed
2018-01-30 15:06:13 +05:00
committed by GitHub
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>