mirror of https://github.com/zulip/zulip.git
invites: Add UI for revoking multiuse invites.
This commit is contained in:
parent
03dcace09d
commit
763eca6ca9
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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" }}
|
||||
|
|
|
@ -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">×</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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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')),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'}),
|
||||
|
|
Loading…
Reference in New Issue