Add administrative panel to allow for user deactivations etc.

We now show a list of users and allow you to deactivate a user using the
same process as `python manage.py deactivate_user`.

We add a new menu item accessible from the gear icon which will eventually
have much more than just this, but we have a good start here.

Here we also add a property to UserProfile which determines whether you're
eligible to access the administration panel, and then have code which shows
the menu option if so.

This introduces a new JS file, admin.js.

(imported from commit 52296fdedb46b4f32d541df43022ffccfb277297)
This commit is contained in:
Luke Faraone 2013-08-12 17:31:23 -04:00
parent 78d8153e6b
commit ecc42bc9f8
11 changed files with 155 additions and 2 deletions

70
static/js/admin.js Normal file
View File

@ -0,0 +1,70 @@
var admin = (function () {
var exports = {};
function populate_users () {
var tb = $("#admin_users_table");
tb.empty();
page_params.people_list.sort(function (a, b) {
return a.full_name.toLowerCase().localeCompare(b.full_name.toLowerCase());
});
$.each(page_params.people_list, function (index, person) {
if (person.email.indexOf("+") === -1 && person.email.indexOf("-bot@") === -1) {
tb.append(templates.render("admin_user_list", {person: person}));
}
});
}
exports.setup_page = function () {
populate_users();
$("#admin_users_table").on("click", ".activation_toggle_button", function (e) {
e.preventDefault();
e.stopPropagation();
$(".active_user_row").removeClass("active_user_row");
// Go up the tree until we find the user row, then grab the email element
$(e.target).closest(".user_row").addClass("active_user_row");
var user_name = $(".active_user_row").find('.user_name').text();
var email = $(".active_user_row").find('.email').text();
$("#deactivation_modal .email").text(email);
$("#deactivation_modal .user_name").text(user_name);
$("#deactivation_modal").modal("show");
});
$("#do_deactivate_button").click(function (e) {
if ($("#deactivation_modal .email").html() !== $(".active_user_row").find('.email').text()) {
blueslip.error("User deactivation canceled due to non-matching fields.");
ui.report_message("Deactivation encountered an error. Please reload and try again.",
$("#home-error"), 'alert-error');
}
$("#deactivation_modal").modal("hide");
$(".active_user_row button").prop("disabled", true).text("Working…");
$.ajax({
type: 'DELETE',
url: '/json/users/' + $(".active_user_row").find('.email').text(),
error: function (xhr, error_type) {
if (xhr.status.toString().charAt(0) === "4") {
$(".active_user_row button").closest("td").html(
$("<p>").addClass("text-error").text($.parseJSON(xhr.responseText).msg)
);
} else {
$(".active_user_row button").text("Failed!");
}
},
success: function () {
$(".active_user_row button").removeClass("btn-danger").text("Deactivated");
$(".active_user_row span").wrap("<strike>");
}
});
});
};
return exports;
}());

View File

@ -130,6 +130,9 @@ function do_hashchange() {
case "#subscriptions": case "#subscriptions":
ui.change_tab_to("#subscriptions"); ui.change_tab_to("#subscriptions");
break; break;
case "#administration":
ui.change_tab_to("#administration");
break;
case "#settings": case "#settings":
ui.change_tab_to("#settings"); ui.change_tab_to("#settings");
break; break;

View File

@ -858,6 +858,9 @@ $(function () {
// Whenever the streams page comes up (from anywhere), populate it. // Whenever the streams page comes up (from anywhere), populate it.
subs_link.on('shown', subs.setup_page); subs_link.on('shown', subs.setup_page);
var admin_link = $('#gear-menu a[href="#administration"]');
admin_link.on('shown', admin.setup_page);
$('#pw_change_link').on('click', function (e) { $('#pw_change_link').on('click', function (e) {
e.preventDefault(); e.preventDefault();
$('#pw_change_link').hide(); $('#pw_change_link').hide();

View File

@ -0,0 +1,23 @@
{{#with person}}
<tr class="user_row" id="user_{{email}}">
<td>
<span class="user_name">{{full_name}}</span>
</td>
<td>
<span class="email">{{email}}</span>
</td>
<td>
<button class="btn activation_toggle_button btn-danger"
{{#inactive}}disabled{{/inactive}}
type="button" name="activation_toggle">
{{#inactive}}
Reactivate
{{/inactive}}
{{^inactive}}
Deactivate
{{/inactive}}
</button>
</div>
</td>
</tr>
{{/with}}

View File

@ -0,0 +1,32 @@
{# Administration panel #}
<div class="row-fluid">
<div class="span12">
<div class="administration">
<h1>Administration</h1>
<h2>Users</h2>
<table class="table table-condensed table-striped">
<tbody id="admin_users_table">
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tbody>
</table>
<div id="deactivation_modal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="deactivation_modal_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="deactivation_modal_label">Deactivate <span class="email"></span></h3>
</div>
<div class="modal-body">
<p>By deactivating <strong><span class="user_name"></span></strong> &lt;<span class="email"></span>&gt;, they will be logged out of Zulip immediately.</p>
<p>Their password will be cleared from our systems, and any bots they maintain will be disabled.</p>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button class="btn btn-danger" id="do_deactivate_button">Deactivate now</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -51,6 +51,11 @@ var page_params = {{ page_params }};
<div class="tab-pane" id="subscriptions"> <div class="tab-pane" id="subscriptions">
{% include "zerver/subscriptions.html" %} {% include "zerver/subscriptions.html" %}
</div> </div>
{% if show_admin %}
<div class="tab-pane" id="administration">
{% include "zephyr/administration.html" %}
</div>
{% endif %}
<div class="tab-pane" id="settings"> <div class="tab-pane" id="settings">
{% include "zerver/settings.html" %} {% include "zerver/settings.html" %}
</div> </div>

View File

@ -58,6 +58,13 @@
</a> </a>
</li> </li>
<li class="divider"></li> <li class="divider"></li>
{% if show_admin %}
<li title="Administration">
<a href="#administration" role="button" data-toggle="tab">
<i class="icon-vector-bolt"></i> Administration
</a>
</li>
{% endif %}
{% if show_invites %} {% if show_invites %}
<li title="Invite coworkers to Zulip"> <li title="Invite coworkers to Zulip">
<a href="#invite-user" role="button" data-toggle="modal"> <a href="#invite-user" role="button" data-toggle="modal">

View File

@ -26,7 +26,7 @@ var globals =
+ ' invite ui util activity timerender MessageList blueslip unread stream_list' + ' invite ui util activity timerender MessageList blueslip unread stream_list'
+ ' onboarding message_edit tab_bar emoji popovers navigate message_tour' + ' onboarding message_edit tab_bar emoji popovers navigate message_tour'
+ ' avatar feature_flags search_suggestion referral stream_color Dict' + ' avatar feature_flags search_suggestion referral stream_color Dict'
+ ' Filter summary' + ' Filter summary admin'
// colorspace.js // colorspace.js
+ ' colorspace' + ' colorspace'

View File

@ -150,6 +150,14 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
objects = UserManager() objects = UserManager()
@property
def show_admin(self):
# Logic to determine if the user should see the administration tools.
# Do NOT use this to check if a user is authorized to perform a specific action!
return 0 < self.userobjectpermission_set.filter(
content_type__name="realm",
permission__codename="administer").count()
def __repr__(self): def __repr__(self):
return (u"<UserProfile: %s %s>" % (self.email, self.realm)).encode("utf-8") return (u"<UserProfile: %s %s>" % (self.email, self.realm)).encode("utf-8")
def __str__(self): def __str__(self):

View File

@ -659,7 +659,8 @@ def home(request):
'nofontface': is_buggy_ua(request.META["HTTP_USER_AGENT"]), 'nofontface': is_buggy_ua(request.META["HTTP_USER_AGENT"]),
'show_debug': 'show_debug':
settings.DEBUG and ('show_debug' in request.GET), settings.DEBUG and ('show_debug' in request.GET),
'show_invites': show_invites 'show_invites': show_invites,
'show_admin': user_profile.show_admin,
}, },
context_instance=RequestContext(request)) context_instance=RequestContext(request))

View File

@ -378,6 +378,7 @@ JS_SPECS = {
'js/compose_fade.js', 'js/compose_fade.js',
'js/compose.js', 'js/compose.js',
'js/stream_color.js', 'js/stream_color.js',
'js/admin.js',
'js/subs.js', 'js/subs.js',
'js/message_edit.js', 'js/message_edit.js',
'js/ui.js', 'js/ui.js',