messages: Add support for quickly deleting all messages in a topic.

This is primarily a feature for onboarding, where an organization
administrator might send a bunch of random test messages as part of
joining, but then want a pristine organization when their users later
join.

But it can theoretically be used for other use cases (e.g. for
moderation or removing threads that are problematic in some way).

Tweaked by tabbott to handle corner cases with
is_history_public_to_subscribers.

Fixes #10912.
This commit is contained in:
kunal-mohta
2019-01-18 22:10:54 +05:30
committed by Tim Abbott
parent 65489b0391
commit ac55a5222c
9 changed files with 164 additions and 3 deletions

View File

@@ -1339,12 +1339,33 @@ run_test('topic_sidebar_actions', () => {
stream_name: 'social',
topic_name: 'lunch',
can_mute_topic: true,
is_admin: false,
};
var html = render('topic_sidebar_actions', args);
var a = $(html).find("a.narrow_to_topic");
assert.equal(a.text().trim(), 'translated: Narrow to topic lunch');
var delete_topic_option = $(html).find("a.sidebar-popover-delete-topic-messages");
assert.equal(delete_topic_option.length, 0);
args = {
is_admin: true,
};
html = render('topic_sidebar_actions', args);
delete_topic_option = $(html).find("a.sidebar-popover-delete-topic-messages");
assert.equal(delete_topic_option.length, 1);
});
run_test('delete_topic_modal', () => {
var args = {
topic_name: 'lunch',
};
var html = render('delete_topic_modal', args);
var modal_body = $(html).find('.modal-body');
assert.equal(modal_body.text().trim(), 'translated: Delete all messages in topic lunch?');
});
run_test('typeahead_list_item', () => {

View File

@@ -641,6 +641,18 @@ exports.delete_message = function (msg_id) {
});
};
exports.delete_topic = function (stream_id, topic_name) {
channel.post({
url: "/json/streams/" + stream_id + "/delete_topic",
data: {
topic_name: topic_name,
},
success: function () {
$('#delete_topic_modal').modal('hide');
},
});
};
exports.handle_narrow_deactivated = function () {
_.each(currently_editing_messages, function (elem, idx) {
if (current_msg_list.get(idx) !== undefined) {

View File

@@ -152,6 +152,7 @@ function build_topic_popover(e) {
topic_name: topic_name,
can_mute_topic: can_mute_topic,
can_unmute_topic: can_unmute_topic,
is_admin: sub.is_admin,
});
$(elt).popover({
@@ -375,6 +376,31 @@ exports.register_topic_handlers = function () {
unread_ops.mark_topic_as_read(stream_id, topic);
e.stopPropagation();
});
// Deleting all message in a topic
$('body').on('click', '.sidebar-popover-delete-topic-messages', function (e) {
var stream_id = topic_popover_stream_id(e);
if (!stream_id) {
return;
}
var topic = $(e.currentTarget).attr('data-topic-name');
var args = {
topic_name: topic,
};
exports.hide_topic_popover();
$('#delete-topic-modal-holder').html(templates.render('delete_topic_modal', args));
$('#do_delete_topic_button').on('click', function () {
message_edit.delete_topic(stream_id, topic);
});
$('#delete_topic_modal').modal('show');
e.stopPropagation();
});
};
return exports;

View File

@@ -0,0 +1,15 @@
<div id="delete_topic_modal" class="modal modal-bg hide fade new-style" tabindex="-1" role="dialog" aria-labelledby="delete_topic_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="delete_topic_modal_label">{{t "Delete topic" }} </h3>
</div>
<div class="modal-body">
<p>{{#tr this}}Delete all messages in topic <b>__topic_name__</b>?{{/tr}}</p>
</div>
<div class="modal-footer">
<button class="button" data-dismiss="modal">{{t "Cancel" }}</button>
<button class="button btn-danger rounded" id="do_delete_topic_button">
{{t "Delete" }}
</button>
</div>
</div>

View File

@@ -31,5 +31,13 @@
</a>
</li>
{{#if is_admin}}
<li>
<a class="sidebar-popover-delete-topic-messages" data-stream-id="{{ stream_id }}" data-topic-name="{{ topic_name }}">
<i class="fa fa-trash" aria-hidden="true"></i>
{{#tr this}}Delete all messages in <b>__topic_name__</b>{{/tr}}
</a>
</li>
{{/if}}
</ul>

View File

@@ -167,5 +167,6 @@
{% include "zerver/app/deprecation_notice.html" %}
<div id="user-profile-modal-holder"></div>
<div class='notifications top-right'></div>
<div id="delete-topic-modal-holder"></div>
</div>
{% endblock %}

View File

@@ -277,6 +277,59 @@ class TopicHistoryTest(ZulipTestCase):
result = self.client_get(endpoint, dict())
self.assert_json_error(result, 'Invalid stream id')
class TopicDeleteTest(ZulipTestCase):
def test_topic_delete(self) -> None:
initial_last_msg_id = self.get_last_message().id
stream_name = 'new_stream'
topic_name = 'new topic 2'
# NON-ADMIN USER
user_profile = self.example_user('hamlet')
self.subscribe(user_profile, stream_name)
# Send message
stream = get_stream(stream_name, user_profile.realm)
self.send_stream_message(user_profile.email, stream_name, topic_name=topic_name)
last_msg_id = self.send_stream_message(user_profile.email, stream_name, topic_name=topic_name)
# Deleting the topic
self.login(user_profile.email, realm=user_profile.realm)
endpoint = '/json/streams/' + str(stream.id) + '/delete_topic'
result = self.client_post(endpoint, {
"topic_name": topic_name
})
self.assert_json_error(result, "Must be an organization administrator")
self.assertEqual(self.get_last_message().id, last_msg_id)
# Make stream private with limited history
do_change_stream_invite_only(stream, invite_only=True,
history_public_to_subscribers=False)
# ADMIN USER subscribed now
user_profile = self.example_user('iago')
self.subscribe(user_profile, stream_name)
self.login(user_profile.email, realm=user_profile.realm)
new_last_msg_id = self.send_stream_message(user_profile.email, stream_name, topic_name=topic_name)
# Now admin deletes all messages in topic -- which should only
# delete new_last_msg_id, i.e. the one sent since they joined.
self.assertEqual(self.get_last_message().id, new_last_msg_id)
result = self.client_post(endpoint, {
"topic_name": topic_name
})
self.assert_json_success(result)
self.assertEqual(self.get_last_message().id, last_msg_id)
# Make the stream's history public to subscribers
do_change_stream_invite_only(stream, invite_only=True,
history_public_to_subscribers=True)
# Delete the topic should now remove all messages
result = self.client_post(endpoint, {
"topic_name": topic_name
})
self.assert_json_success(result)
self.assertEqual(self.get_last_message().id, initial_last_msg_id)
class TestCrossRealmPMs(ZulipTestCase):
def make_realm(self, domain: str) -> Realm:
realm = Realm.objects.create(string_id=domain, invite_required=False)

View File

@@ -22,16 +22,17 @@ from zerver.lib.actions import bulk_remove_subscriptions, \
do_create_default_stream_group, do_add_streams_to_default_stream_group, \
do_remove_streams_from_default_stream_group, do_remove_default_stream_group, \
do_change_default_stream_group_description, do_change_default_stream_group_name, \
prep_stream_welcome_message, do_change_stream_announcement_only
prep_stream_welcome_message, do_change_stream_announcement_only, \
do_delete_messages
from zerver.lib.response import json_success, json_error, json_response
from zerver.lib.streams import access_stream_by_id, access_stream_by_name, \
check_stream_name, check_stream_name_available, filter_stream_authorization, \
list_to_streams, access_stream_for_delete_or_update, access_default_stream_group_by_id
from zerver.lib.topic import get_topic_history_for_stream
from zerver.lib.topic import get_topic_history_for_stream, messages_for_topic
from zerver.lib.validator import check_string, check_int, check_list, check_dict, \
check_bool, check_variable_type, check_capped_string, check_color, check_dict_only
from zerver.models import UserProfile, Stream, Realm, Subscription, \
Recipient, get_recipient, get_stream, \
Recipient, Message, UserMessage, get_recipient, get_stream, \
get_system_bot, get_active_user
from collections import defaultdict
@@ -453,6 +454,25 @@ def get_topics_backend(request: HttpRequest, user_profile: UserProfile,
return json_success(dict(topics=result))
@require_realm_admin
@has_request_variables
def delete_in_topic(request: HttpRequest, user_profile: UserProfile,
stream_id: int=REQ(converter=to_non_negative_int),
topic_name: str=REQ("topic_name")) -> HttpResponse:
(stream, recipient, sub) = access_stream_by_id(user_profile, stream_id)
messages = messages_for_topic(stream.id, topic_name)
if not stream.is_history_public_to_subscribers():
# Don't allow the user to delete messages that they don't have access to.
deletable_message_ids = UserMessage.objects.filter(
user_profile=user_profile, message_id__in=messages).values_list("message_id", flat=True)
messages = [message for message in messages if message.id in
deletable_message_ids]
do_delete_messages(user_profile, messages)
return json_success()
@authenticated_json_post_view
@has_request_variables
def json_stream_exists(request: HttpRequest, user_profile: UserProfile, stream_name: str=REQ("stream"),

View File

@@ -329,6 +329,11 @@ v1_api_and_json_patterns = [
url(r'^streams/(?P<stream_id>\d+)$', rest_dispatch,
{'PATCH': 'zerver.views.streams.update_stream_backend',
'DELETE': 'zerver.views.streams.deactivate_stream_backend'}),
# Delete topic in stream
url(r'^streams/(?P<stream_id>\d+)/delete_topic$', rest_dispatch,
{'POST': 'zerver.views.streams.delete_in_topic'}),
url(r'^default_streams$', rest_dispatch,
{'POST': 'zerver.views.streams.add_default_stream',
'DELETE': 'zerver.views.streams.remove_default_stream'}),