invites: Add UI for revoking multiuse invites.

This commit is contained in:
Vishnu Ks 2019-02-15 18:09:25 +00:00 committed by Tim Abbott
parent 03dcace09d
commit 763eca6ca9
11 changed files with 188 additions and 36 deletions

View File

@ -294,9 +294,6 @@ run_test('admin_invites_list', () => {
var span = $(html).find(".email:first");
assert.equal(span.text(), "alice@zulip.com");
var icon = $(html).find(".fa-bolt");
assert.equal(icon.attr('title'), "translated: Invited as administrator");
});
run_test('admin_tab', () => {
@ -1050,6 +1047,18 @@ run_test('reminder_popover_content', () => {
assert.equal(link.text().trim(), 'translated: Select date and time');
});
run_test('revoke_invite_modal', () => {
var args = {
is_multiuse: false,
email: "iago@zulip.com",
};
var html = "<div>";
html += render('revoke-invite-modal', args);
html += "</div>";
assert.equal($(html).find("p strong").text(), "iago@zulip.com");
});
run_test('settings_tab', () => {
var page_param_checkbox_options = {
enable_stream_desktop_notifications: true,

View File

@ -50,7 +50,7 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
case 'invites_changed':
if ($('#admin-invites-list').length) {
settings_invites.set_up();
settings_invites.set_up(false);
}
break;

View File

@ -65,7 +65,12 @@ function populate_invites(invites_data) {
filter: {
element: invites_table.closest(".settings-section").find(".search"),
callback: function (item, value) {
return item.email.toLowerCase().indexOf(value) >= 0;
var referrer_email_matched = item.ref.toLowerCase().indexOf(value) >= 0;
if (item.is_multiuse) {
return referrer_email_matched;
}
var invitee_email_matched = item.email.toLowerCase().indexOf(value) >= 0;
return referrer_email_matched || invitee_email_matched;
},
},
}).init();
@ -76,17 +81,23 @@ function populate_invites(invites_data) {
function do_revoke_invite() {
var modal_invite_id = $("#revoke_invite_modal #do_revoke_invite_button").attr("data-invite-id");
var modal_is_multiuse = $("#revoke_invite_modal #do_revoke_invite_button").attr("data-is-multiuse");
var revoke_button = meta.current_revoke_invite_user_modal_row.find("button.revoke");
if (modal_invite_id !== meta.invite_id) {
if (modal_invite_id !== meta.invite_id || modal_is_multiuse !== meta.is_multiuse) {
blueslip.error("Invite revoking canceled due to non-matching fields.");
ui_report.message(i18n.t("Resending encountered an error. Please reload and try again."),
$("#home-error"), 'alert-error');
}
$("#revoke_invite_modal").modal("hide");
revoke_button.prop("disabled", true).text(i18n.t("Working…"));
var url = '/json/invites/' + meta.invite_id;
if (modal_is_multiuse === "true") {
url = '/json/invites/multiuse/' + meta.invite_id;
}
channel.del({
url: '/json/invites/' + meta.invite_id,
url: url,
error: function (xhr) {
ui_report.generic_row_button_error(xhr, revoke_button);
},
@ -96,8 +107,11 @@ function do_revoke_invite() {
});
}
exports.set_up = function () {
exports.set_up = function (initialize_event_handlers) {
meta.loaded = true;
if (typeof initialize_event_handlers === 'undefined') {
initialize_event_handlers = true;
}
// create loading indicators
loading.make_indicator($('#admin_page_invites_loading_indicator'));
@ -107,29 +121,38 @@ exports.set_up = function () {
url: '/json/invites',
idempotent: true,
timeout: 10 * 1000,
success: exports.on_load_success,
success: function (data) {
exports.on_load_success(data, initialize_event_handlers);
},
error: failed_listing_invites,
});
};
exports.on_load_success = function (invites_data) {
exports.on_load_success = function (invites_data, initialize_event_handlers) {
meta.loaded = true;
populate_invites(invites_data);
if (!initialize_event_handlers) {
return;
}
$(".admin_invites_table").on("click", ".revoke", function (e) {
// This click event must not get propagated to parent container otherwise the modal
// will not show up because of a call to `close_active_modal` in `settings.js`.
e.preventDefault();
e.stopPropagation();
var row = $(e.target).closest(".invite_row");
var email = row.find('.email').text();
var referred_by = row.find('.referred_by').text();
meta.current_revoke_invite_user_modal_row = row;
meta.invite_id = $(e.currentTarget).attr("data-invite-id");
$("#revoke_invite_modal .email").text(email);
meta.is_multiuse = $(e.currentTarget).attr("data-is-multiuse");
var ctx = {is_multiuse: meta.is_multiuse === "true", email: email, referred_by: referred_by};
var rendered_revoke_modal = templates.render("revoke-invite-modal", ctx);
$("#revoke_invite_modal_holder").html(rendered_revoke_modal);
$("#revoke_invite_modal #do_revoke_invite_button").attr("data-invite-id", meta.invite_id);
$("#revoke_invite_modal #do_revoke_invite_button").attr("data-is-multiuse", meta.is_multiuse);
$("#revoke_invite_modal").modal("show");
$("#do_revoke_invite_button").unbind("click");
$("#do_revoke_invite_button").click(do_revoke_invite);
});
$(".admin_invites_table").on("click", ".resend", function (e) {
@ -148,8 +171,6 @@ exports.on_load_success = function (invites_data) {
$("#resend_invite_modal").modal("show");
});
$("#do_revoke_invite_button").click(do_revoke_invite);
$("#do_resend_invite_button").click(function () {
var modal_invite_id = $("#resend_invite_modal #do_resend_invite_button").attr("data-invite-id");
var resend_button = meta.current_resend_invite_user_modal_row.find("button.resend");

View File

@ -1,9 +1,10 @@
{{#with invite}}
<tr class="invite_row">
<td>
{{#if is_multiuse}}
<span class="email">{{t 'Invite link'}}</span>
{{else}}
<span class="email">{{email}}</span>
{{#if invited_as_admin}}
<i title="{{t 'Invited as administrator'}}" class="fa fa-bolt invited-as-admin"></i>
{{/if}}
</td>
<td>
@ -16,12 +17,14 @@
<span>{{invited_as_text}}</span>
</td>
<td>
<button class="button rounded small revoke btn-danger" data-invite-id="{{id}}">
<button class="button rounded small revoke btn-danger" data-invite-id="{{id}}" data-is-multiuse="{{is_multiuse}}">
{{t "Revoke" }}
</button>
{{#unless is_multiuse}}
<button class="button rounded small resend btn-warning" data-invite-id="{{id}}">
{{t "Resend" }}
</button>
{{/unless}}
</td>
</tr>
{{/with}}

View File

@ -1,4 +1,6 @@
<div class="alert" id="organization-status"></div>
<div id="revoke_invite_modal_holder"></div>
{{ partial "admin-settings-modals"}}
{{ partial "organization-profile-admin" }}

View File

@ -1,10 +1,18 @@
<div id="revoke_invite_modal" class="modal modal-bg hide fade" tabindex="-1" role="dialog" aria-labelledby="revoke_invite_modal_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="{{t 'Close' }}"><span aria-hidden="true">&times;</span></button>
<h3 id="revoke_invite_modal_label">{{#tr this}}Revoke invitation to <span class="email"></span>{{/tr}}</h3>
{{#if is_multiuse}}
<h3 id="revoke_invite_modal_label">{{#tr this}}Revoke invitation link{{/tr}}</h3>
{{else}}
<h3 id="revoke_invite_modal_label">{{#tr this}}Revoke invitation to __email__{{/tr}}</h3>
{{/if}}
</div>
<div class="modal-body">
<p>{{#tr this}}Are you sure you want to revoke the invitation to <strong><span class="email"></span></strong>?{{/tr}}</p>
<div class="modal-body" id="revoke_invite_message">
{{#if is_multiuse}}
<p>{{#tr this}}Are you sure you want to revoke this invitation link created by <strong>__referred_by__</strong>?{{/tr}}</p>
{{else}}
<p>{{#tr this}}Are you sure you want to revoke the invitation to <strong>__email__</strong>?{{/tr}}</p>
{{/if}}
</div>
<div class="modal-footer">
<button class="button rounded" data-dismiss="modal">{{t "Cancel" }}</button>

View File

@ -4902,8 +4902,19 @@ def do_get_user_invites(user_profile: UserProfile) -> List[Dict[str, Any]]:
ref=invitee.referred_by.email,
invited=datetime_to_timestamp(invitee.invited_at),
id=invitee.id,
invited_as=invitee.invited_as))
invited_as=invitee.invited_as,
is_multiuse=False))
multiuse_confirmation_objs = Confirmation.objects.filter(realm=user_profile.realm,
type=Confirmation.MULTIUSE_INVITE,
date_sent__gte=lowest_datetime)
for confirmation_obj in multiuse_confirmation_objs:
invite = confirmation_obj.content_object
invites.append(dict(ref=invite.referred_by.email,
invited=datetime_to_timestamp(confirmation_obj.date_sent),
id=invite.id,
invited_as=invite.invited_as,
is_multiuse=True))
return invites
def do_create_multiuse_invite_link(referred_by: UserProfile, invited_as: int,
@ -4914,6 +4925,7 @@ def do_create_multiuse_invite_link(referred_by: UserProfile, invited_as: int,
invite.streams.set(streams)
invite.invited_as = invited_as
invite.save()
notify_invites_changed(referred_by)
return create_confirmation_link(invite, realm.host, Confirmation.MULTIUSE_INVITE)
def do_revoke_user_invite(prereg_user: PreregistrationUser) -> None:
@ -4921,7 +4933,7 @@ def do_revoke_user_invite(prereg_user: PreregistrationUser) -> None:
# Delete both the confirmation objects and the prereg_user object.
# TODO: Probably we actaully want to set the confirmation objects
# to a "revoked" status so that we can give the user a better
# to a "revoked" status so that we can give the invited user a better
# error message.
content_type = ContentType.objects.get_for_model(PreregistrationUser)
Confirmation.objects.filter(content_type=content_type,
@ -4930,6 +4942,13 @@ def do_revoke_user_invite(prereg_user: PreregistrationUser) -> None:
clear_scheduled_invitation_emails(email)
notify_invites_changed(prereg_user)
def do_revoke_multi_use_invite(multiuse_invite: MultiuseInvite) -> None:
content_type = ContentType.objects.get_for_model(MultiuseInvite)
Confirmation.objects.filter(content_type=content_type,
object_id=multiuse_invite.id).delete()
multiuse_invite.delete()
notify_invites_changed(multiuse_invite.referred_by)
def do_resend_user_invite_email(prereg_user: PreregistrationUser) -> int:
check_invite_limit(prereg_user.referred_by.realm, 1)

View File

@ -16,7 +16,7 @@ from zerver.models import (
get_client, get_realm, get_stream_recipient, get_stream,
Message, RealmDomain, Recipient, UserMessage, UserPresence, UserProfile,
Realm, Subscription, Stream, flush_per_request_caches, UserGroup, Service,
Attachment, PreregistrationUser, get_user_by_delivery_email
Attachment, PreregistrationUser, get_user_by_delivery_email, MultiuseInvite
)
from zerver.lib.actions import (
@ -51,6 +51,7 @@ from zerver.lib.actions import (
do_change_user_delivery_email,
do_create_user,
do_create_default_stream_group,
do_create_multiuse_invite_link,
do_deactivate_stream,
do_deactivate_user,
do_delete_messages,
@ -69,6 +70,7 @@ from zerver.lib.actions import (
do_remove_realm_filter,
do_remove_streams_from_default_stream_group,
do_rename_stream,
do_revoke_multi_use_invite,
do_revoke_user_invite,
do_set_realm_authentication_methods,
do_set_realm_message_editing,
@ -947,6 +949,23 @@ class EventsRegisterTest(ZulipTestCase):
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_create_multiuse_invite_event(self) -> None:
schema_checker = self.check_events_dict([
('type', equals('invites_changed')),
])
self.user_profile = self.example_user('iago')
streams = []
for stream_name in ["Denmark", "Verona"]:
streams.append(get_stream(stream_name, self.user_profile.realm))
events = self.do_test(
lambda: do_create_multiuse_invite_link(self.user_profile, PreregistrationUser.INVITE_AS['MEMBER'], streams),
state_change_expected=False,
)
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_revoke_user_invite_event(self) -> None:
schema_checker = self.check_events_dict([
('type', equals('invites_changed')),
@ -965,6 +984,25 @@ class EventsRegisterTest(ZulipTestCase):
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_revoke_multiuse_invite_event(self) -> None:
schema_checker = self.check_events_dict([
('type', equals('invites_changed')),
])
self.user_profile = self.example_user('iago')
streams = []
for stream_name in ["Denmark", "Verona"]:
streams.append(get_stream(stream_name, self.user_profile.realm))
do_create_multiuse_invite_link(self.user_profile, PreregistrationUser.INVITE_AS['MEMBER'], streams)
multiuse_object = MultiuseInvite.objects.get()
events = self.do_test(
lambda: do_revoke_multi_use_invite(multiuse_object),
state_change_expected=False,
)
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_invitation_accept_invite_event(self) -> None:
schema_checker = self.check_events_dict([
('type', equals('invites_changed')),

View File

@ -1254,7 +1254,7 @@ class InvitationsTestCase(InviteUserBase):
"""
A GET call to /json/invites returns all unexpired invitations.
"""
realm = get_realm("zulip")
days_to_activate = getattr(settings, 'INVITATION_LINK_VALIDITY_DAYS', "Wrong")
active_value = getattr(confirmation_settings, 'STATUS_ACTIVE', "Wrong")
self.assertNotEqual(days_to_activate, "Wrong")
@ -1273,10 +1273,19 @@ class InvitationsTestCase(InviteUserBase):
referred_by=user_profile, status=active_value)
prereg_user_three.save()
multiuse_invite_one = MultiuseInvite.objects.create(referred_by=self.example_user("hamlet"), realm=realm)
create_confirmation_link(multiuse_invite_one, realm.host, Confirmation.MULTIUSE_INVITE)
multiuse_invite_two = MultiuseInvite.objects.create(referred_by=self.example_user("othello"), realm=realm)
create_confirmation_link(multiuse_invite_two, realm.host, Confirmation.MULTIUSE_INVITE)
confirmation = Confirmation.objects.last()
confirmation.date_sent = expired_datetime
confirmation.save()
result = self.client_get("/json/invites")
self.assertEqual(result.status_code, 200)
self.assert_in_success_response(["TestOne@zulip.com"], result)
self.assert_not_in_success_response(["TestTwo@zulip.com", "TestThree@zulip.com"], result)
self.assert_in_success_response(["TestOne@zulip.com", "hamlet@zulip.com"], result)
self.assert_not_in_success_response(["TestTwo@zulip.com", "TestThree@zulip.com", "othello@zulip.com"], result)
def test_successful_delete_invitation(self) -> None:
"""
@ -1302,6 +1311,30 @@ class InvitationsTestCase(InviteUserBase):
lambda: ScheduledEmail.objects.get(address__iexact=invitee,
type=ScheduledEmail.INVITATION_REMINDER))
def test_delete_multiuse_invite(self) -> None:
"""
A DELETE call to /json/invites/multiuse<ID> should delete the
multiuse_invite.
"""
self.login(self.example_email("iago"))
zulip_realm = get_realm("zulip")
multiuse_invite = MultiuseInvite.objects.create(referred_by=self.example_user("hamlet"), realm=zulip_realm)
create_confirmation_link(multiuse_invite, zulip_realm.host, Confirmation.MULTIUSE_INVITE)
result = self.client_delete('/json/invites/multiuse/' + str(multiuse_invite.id))
self.assertEqual(result.status_code, 200)
self.assertIsNone(MultiuseInvite.objects.filter(id=multiuse_invite.id).first())
# Test that trying to double-delete fails
error_result = self.client_delete('/json/invites/multiuse/' + str(multiuse_invite.id))
self.assert_json_error(error_result, "No such invitation")
# Test deleting multiuse invite from another realm
mit_realm = get_realm("zephyr")
multiuse_invite_in_mit = MultiuseInvite.objects.create(referred_by=self.mit_user("sipbtest"), realm=mit_realm)
create_confirmation_link(multiuse_invite_in_mit, mit_realm.host, Confirmation.MULTIUSE_INVITE)
error_result = self.client_delete('/json/invites/multiuse/' + str(multiuse_invite_in_mit.id))
self.assert_json_error(error_result, "No such invitation")
def test_successful_resend_invitation(self) -> None:
"""
A POST call to /json/invites/<ID>/resend should send an invitation reminder email

View File

@ -2,16 +2,15 @@ from django.http import HttpRequest, HttpResponse
from django.utils.translation import ugettext as _
from typing import List, Optional, Set
from zerver.decorator import require_realm_admin, \
require_non_guest_human_user
from zerver.lib.actions import do_invite_users, do_revoke_user_invite, do_resend_user_invite_email, \
do_get_user_invites, do_create_multiuse_invite_link
from zerver.decorator import require_realm_admin, require_non_guest_human_user
from zerver.lib.actions import do_invite_users, do_revoke_user_invite, \
do_revoke_multi_use_invite, do_resend_user_invite_email, \
get_default_subs, do_get_user_invites, do_create_multiuse_invite_link
from zerver.lib.request import REQ, has_request_variables, JsonableError
from zerver.lib.response import json_success, json_error
from zerver.lib.response import json_success, json_error, json_response
from zerver.lib.streams import access_stream_by_id
from zerver.lib.validator import check_list, check_int
from zerver.models import PreregistrationUser, Stream, UserProfile
from zerver.lib.validator import check_string, check_list, check_bool, check_int
from zerver.models import PreregistrationUser, Stream, UserProfile, MultiuseInvite
import re
@ -85,6 +84,22 @@ def revoke_user_invite(request: HttpRequest, user_profile: UserProfile,
do_revoke_user_invite(prereg_user)
return json_success()
@require_realm_admin
@has_request_variables
def revoke_multiuse_invite(request: HttpRequest, user_profile: UserProfile,
invite_id: int) -> HttpResponse:
try:
invite = MultiuseInvite.objects.get(id=invite_id)
except MultiuseInvite.DoesNotExist:
raise JsonableError(_("No such invitation"))
if invite.realm != user_profile.realm:
raise JsonableError(_("No such invitation"))
do_revoke_multi_use_invite(invite)
return json_success()
@require_realm_admin
@has_request_variables
def resend_user_invite_email(request: HttpRequest, user_profile: UserProfile,

View File

@ -165,6 +165,10 @@ v1_api_and_json_patterns = [
# invites/multiuse -> zerver.views.invite
url(r'^invites/multiuse$', rest_dispatch,
{'POST': 'zerver.views.invite.generate_multiuse_invite_backend'}),
# invites/multiuse -> zerver.views.invite
url(r'^invites/multiuse/(?P<invite_id>[0-9]+)$', rest_dispatch,
{'DELETE': 'zerver.views.invite.revoke_multiuse_invite'}),
# mark messages as read (in bulk)
url(r'^mark_all_as_read$', rest_dispatch,
{'POST': 'zerver.views.messages.mark_all_as_read'}),