mirror of
https://github.com/zulip/zulip.git
synced 2025-11-02 13:03:29 +00:00
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:
@@ -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', () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
15
static/templates/delete_topic_modal.handlebars
Normal file
15
static/templates/delete_topic_modal.handlebars
Normal 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">×</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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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'}),
|
||||
|
||||
Reference in New Issue
Block a user