Make default_streams web controllable.

Fixes: #665
This commit is contained in:
Vishnu Ks
2016-05-21 01:38:42 +05:30
committed by Tim Abbott
parent 0c322403a6
commit f9f31b79d0
11 changed files with 277 additions and 13 deletions

View File

@@ -137,6 +137,52 @@ casper.waitWhileSelector('.emoji_row', function () {
casper.test.assertDoesntExist('.emoji_row'); casper.test.assertDoesntExist('.emoji_row');
}); });
function get_suggestions(str) {
casper.then(function () {
casper.evaluate(function (str) {
$('.create_default_stream')
.focus()
.val(str)
.trigger($.Event('keyup', { which: 0 }));
}, str);
});
}
function select_from_suggestions(item) {
casper.then(function () {
casper.evaluate(function (item) {
var tah = $('.create_default_stream').data().typeahead;
tah.mouseenter({
currentTarget: $('.typeahead:visible li:contains("'+item+'")')[0]
});
tah.select();
}, {item: item});
});
}
// Test default stream creation and addition
casper.then(function () {
casper.click('#settings-dropdown');
casper.click('a[href^="#subscriptions"]');
casper.click('#settings-dropdown');
casper.click('a[href^="#administration"]');
var stream_name = "Scotland";
// It matches with all the stream names which has 'O' as a substring (Rome, Scotland, Verona etc).
// I used 'O' to make sure that it works even if there are multiple suggestions.
// Capital 'O' is used instead of small 'o' to make sure that the suggestions are not case sensitive.
get_suggestions("O");
select_from_suggestions(stream_name);
casper.waitForSelector('.default_stream_row[id='+stream_name+']', function () {
casper.test.assertSelectorHasText('.default_stream_row[id='+stream_name+'] .default_stream_name', stream_name);
});
casper.waitForSelector('.default_stream_row[id='+stream_name+']', function () {
casper.test.assertSelectorHasText('.default_stream_row[id='+stream_name+'] .default_stream_name', stream_name);
casper.click('.default_stream_row[id='+stream_name+'] button.remove-default-stream');
});
casper.waitWhileSelector('.default_stream_row[id='+stream_name+']', function () {
casper.test.assertDoesntExist('.default_stream_row[id='+stream_name+']');
});
});
// TODO: Test stream deletion // TODO: Test stream deletion
common.then_log_out(); common.then_log_out();

View File

@@ -86,6 +86,19 @@ function render(template_name, args) {
global.write_test_output("admin_tab.handlebars", html); global.write_test_output("admin_tab.handlebars", html);
}()); }());
(function admin_default_streams_list() {
var html = '<table>';
var streams = ['devel', 'trac', 'zulip'];
_.each(streams, function (stream) {
var args = {stream: {name: stream, invite_only: false}};
html += render('admin_default_streams_list', args);
});
html += "</table>";
var span = $(html).find(".default_stream_name:first");
assert.equal(span.text(), "devel");
global.write_test_output("admin_default_streams_list.handlebars", html);
}());
(function admin_streams_list() { (function admin_streams_list() {
var html = '<table>'; var html = '<table>';
var streams = ['devel', 'trac', 'zulip']; var streams = ['devel', 'trac', 'zulip'];

View File

@@ -1,6 +1,7 @@
var admin = (function () { var admin = (function () {
var exports = {}; var exports = {};
var all_streams = [];
exports.show_or_hide_menu_item = function () { exports.show_or_hide_menu_item = function () {
var item = $('.admin-menu-item').expectOne(); var item = $('.admin-menu-item').expectOne();
@@ -67,6 +68,7 @@ function populate_users (realm_people_data) {
function populate_streams (streams_data) { function populate_streams (streams_data) {
var streams_table = $("#admin_streams_table").expectOne(); var streams_table = $("#admin_streams_table").expectOne();
all_streams = streams_data;
streams_table.find("tr.stream_row").remove(); streams_table.find("tr.stream_row").remove();
_.each(streams_data.streams, function (stream) { _.each(streams_data.streams, function (stream) {
streams_table.append(templates.render("admin_streams_list", {stream: stream})); streams_table.append(templates.render("admin_streams_list", {stream: stream}));
@@ -74,6 +76,55 @@ function populate_streams (streams_data) {
loading.destroy_indicator($('#admin_page_streams_loading_indicator')); loading.destroy_indicator($('#admin_page_streams_loading_indicator'));
} }
function populate_default_streams(streams_data) {
var default_streams_table = $("#admin_default_streams_table").expectOne();
_.each(streams_data, function (stream) {
default_streams_table.append(templates.render("admin_default_streams_list", {stream: stream}));
});
loading.destroy_indicator($('#admin_page_default_streams_loading_indicator'));
}
function get_non_default_streams_names(streams_data) {
var non_default_streams_names = [];
var default_streams_names = [];
_.each(page_params.realm_default_streams, function (default_stream) {
default_streams_names.push(default_stream.name);
});
_.each(streams_data.streams, function (stream) {
if (default_streams_names.indexOf(stream.name) < 0) {
non_default_streams_names.push(stream.name);
}
});
return non_default_streams_names;
}
exports.update_default_streams_table = function () {
$("#admin_default_streams_table").expectOne().find("tr.default_stream_row").remove();
populate_default_streams(page_params.realm_default_streams);
};
function make_stream_default(stream_name) {
var data = {
stream_name: stream_name
};
var default_streams_table = $("#admin_default_streams_table").expectOne();
channel.put({
url: '/json/default_streams',
data: data,
error: function (xhr, error_type) {
if (xhr.status.toString().charAt(0) === "4") {
$(".active_stream_row button").closest("td").html(
$("<p>").addClass("text-error").text($.parseJSON(xhr.responseText).msg));
} else {
$(".active_stream_row button").text("Failed!");
}
}
});
}
exports.populate_emoji = function (emoji_data) { exports.populate_emoji = function (emoji_data) {
var emoji_table = $('#admin_emoji_table').expectOne(); var emoji_table = $('#admin_emoji_table').expectOne();
emoji_table.find('tr.emoji_row').remove(); emoji_table.find('tr.emoji_row').remove();
@@ -123,7 +174,7 @@ exports.setup_page = function () {
// Populate streams table // Populate streams table
channel.get({ channel.get({
url: '/json/streams?include_public=true&include_subscribed=true', url: '/json/streams?include_public=true&include_subscribed=true&include_default=true',
timeout: 10*1000, timeout: 10*1000,
idempotent: true, idempotent: true,
success: populate_streams, success: populate_streams,
@@ -132,6 +183,7 @@ exports.setup_page = function () {
// Populate emoji table // Populate emoji table
exports.populate_emoji(page_params.realm_emoji); exports.populate_emoji(page_params.realm_emoji);
exports.update_default_streams_table();
// Setup click handlers // Setup click handlers
$(".admin_user_table").on("click", ".deactivate", function (e) { $(".admin_user_table").on("click", ".deactivate", function (e) {
@@ -164,6 +216,51 @@ exports.setup_page = function () {
$("#deactivation_stream_modal").modal("show"); $("#deactivation_stream_modal").modal("show");
}); });
$(".admin_default_stream_table").on("click", ".remove-default-stream", function (e) {
e.preventDefault();
e.stopPropagation();
$(".active_default_stream_row").removeClass("active_default_stream_row");
var row = $(e.target).closest(".default_stream_row");
row.addClass("active_default_stream_row");
var stream_name = row.find('.default_stream_name').text();
channel.del({
url: '/json/default_streams'+ '?' + $.param({"stream_name": stream_name}),
error: function (xhr, error_type) {
if (xhr.status.toString().charAt(0) === "4") {
$(".active_default_stream_row button").closest("td").html(
$("<p>").addClass("text-error").text($.parseJSON(xhr.responseText).msg));
} else {
$(".active_default_stream_row button").text("Failed!");
}
},
success: function () {
var row = $(".active_default_stream_row");
row.remove();
}
});
});
$('.create_default_stream').keypress(function (e) {
if (e.which === 13) {
e.preventDefault();
e.stopPropagation();
}
});
$('.create_default_stream').typeahead({
items: 5,
fixed: true,
source: function (query) {
return get_non_default_streams_names(all_streams);
},
highlight: true,
updater: function (stream_name) {
make_stream_default(stream_name);
}
});
$(".admin_bot_table").on("click", ".deactivate", function (e) { $(".admin_bot_table").on("click", ".deactivate", function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View File

@@ -119,8 +119,13 @@ function get_events_success(events) {
if (event.op === 'update') { if (event.op === 'update') {
// Legacy: Stream properties are still managed by subs.js on the client side. // Legacy: Stream properties are still managed by subs.js on the client side.
subs.update_subscription_properties(event.name, event.property, event.value); subs.update_subscription_properties(event.name, event.property, event.value);
admin.update_default_streams_table();
} }
break; break;
case 'default_streams':
page_params.realm_default_streams = event.default_streams;
admin.update_default_streams_table();
break;
case 'subscription': case 'subscription':
if (event.op === 'add') { if (event.op === 'add') {
_.each(event.subscriptions, function (sub) { _.each(event.subscriptions, function (sub) {

View File

@@ -3246,7 +3246,8 @@ div.edit_bot {
.edit_bot_form .control-label, .edit_bot_form .control-label,
#create_bot_form .control-label, #create_bot_form .control-label,
.admin-emoji-form .control-label { .admin-emoji-form .control-label,
.default-stream-form .control-label {
width: 10em; width: 10em;
text-align: right; text-align: right;
margin-right: 20px; margin-right: 20px;
@@ -3501,7 +3502,8 @@ div.edit_bot {
#settings .bot-information-box, #settings .bot-information-box,
#settings .add-new-bot-box, #settings .add-new-bot-box,
#emoji-settings .add-new-emoji-box { #emoji-settings .add-new-emoji-box,
#admin-default-streams-list .add-new-default-stream-box{
background: #e3e3e3; background: #e3e3e3;
padding: 10px; padding: 10px;
margin-left: 38px; margin-left: 38px;
@@ -3513,10 +3515,17 @@ div.edit_bot {
} }
#settings .add-new-bot-box, #settings .add-new-bot-box,
#emoji-settings .add-new-emoji-box { #emoji-settings .add-new-emoji-box,
#admin-default-streams-list .add-new-default-stream-box {
background: #cbe3cb; background: #cbe3cb;
} }
.new-default-stream-section-title {
font-size: 18px;
font-weight: 300;
padding-bottom: 10px;
}
#settings #get_api_key_box, #settings #get_api_key_box,
#settings #show_api_key_box, #settings #show_api_key_box,
#settings #api_key_button_box .control-group { #settings #api_key_button_box .control-group {

View File

@@ -0,0 +1,13 @@
{{#with stream}}
<tr class="default_stream_row" id="{{name}}">
<td>
{{#if invite_only}}<i class="icon-vector-lock "></i>{{/if}}
<span class="default_stream_name">{{name}}</span>
</td>
<td>
<button class="btn remove-default-stream btn-danger">
{{t "Remove from default" }}
</button>
</td>
</tr>
{{/with}}

View File

@@ -19,6 +19,9 @@
<li role="presentation"> <li role="presentation">
<a href="#streams" aria-controls="streams" role="tab" data-toggle="tab"><i class="icon-vector-exchange settings-section-icon"></i> {{t "Streams Deletion" }}</a> <a href="#streams" aria-controls="streams" role="tab" data-toggle="tab"><i class="icon-vector-exchange settings-section-icon"></i> {{t "Streams Deletion" }}</a>
</li> </li>
<li role="presentation">
<a href="#default-streams" aria-controls="default-streams" role="tab" data-toggle="tab"><i class="icon-vector-exchange settings-section-icon"></i> {{t "Default Streams" }}</a>
</li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="organization"> <div role="tabpanel" class="tab-pane active" id="organization">
@@ -163,6 +166,31 @@
<div id="admin_page_streams_loading_indicator"></div> <div id="admin_page_streams_loading_indicator"></div>
</div> </div>
</div> </div>
<div role="tabpanel" class="tab-pane" id="default-streams">
<div id="admin-default-streams-list" class="settings-section">
<div class="settings-section-title"><i class="icon-vector-exchange settings-section-icon"></i>
{{t "Default Streams"}}</div>
<div class="admin-table-wrapper">
<p>{{#tr this}}Configure the default streams new users are subscribed to when joining the {{domain}} organization.{{/tr}}</p>
<table class="table table-condensed table-striped">
<tbody id="admin_default_streams_table" class="admin_default_stream_table">
<th>{{t "Name" }}</th>
<th class="actions">{{t "Actions" }}</th>
</tbody>
</table>
</div>
<div id="admin_page_default_streams_loading_indicator"></div>
<form class="form-horizontal default-stream-form">
<div class="add-new-default-stream-box">
<div class="new-default-stream-section-title">{{t "Add New Default Stream" }}</div>
<div class="control-group" id="default_stream_inputs">
<label for="default_stream_name" class="control-label">{{t "Stream Name" }}</label>
<input class="create_default_stream" type="text" placeholder="{{t "Stream Name" }}" name="stream_name" autocomplete="off"></input>
</div>
</div>
</form>
</div>
</div>
</div> </div>
<div id="deactivation_user_modal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="deactivation_user_modal_label" aria-hidden="true"> <div id="deactivation_user_modal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="deactivation_user_modal_label" aria-hidden="true">

View File

@@ -2025,11 +2025,21 @@ def set_default_streams(realm, stream_names):
'domain': realm.domain, 'domain': realm.domain,
'streams': stream_names}) 'streams': stream_names})
def notify_default_streams(realm):
# type: (Realm) -> None
event = dict(
type="default_streams",
default_streams=streams_to_dicts_sorted(get_default_streams_for_realm(realm))
)
send_event(event, active_user_ids(realm))
def do_add_default_stream(realm, stream_name): def do_add_default_stream(realm, stream_name):
# type: (Realm, text_type) -> None # type: (Realm, text_type) -> None
stream, _ = create_stream_if_needed(realm, stream_name) stream, _ = create_stream_if_needed(realm, stream_name)
if not DefaultStream.objects.filter(realm=realm, stream=stream).exists(): if not DefaultStream.objects.filter(realm=realm, stream=stream).exists():
DefaultStream.objects.create(realm=realm, stream=stream) DefaultStream.objects.create(realm=realm, stream=stream)
notify_default_streams(realm)
def do_remove_default_stream(realm, stream_name): def do_remove_default_stream(realm, stream_name):
# type: (Realm, text_type) -> None # type: (Realm, text_type) -> None
@@ -2037,6 +2047,7 @@ def do_remove_default_stream(realm, stream_name):
if stream is None: if stream is None:
raise JsonableError(_("Stream does not exist")) raise JsonableError(_("Stream does not exist"))
DefaultStream.objects.filter(realm=realm, stream=stream).delete() DefaultStream.objects.filter(realm=realm, stream=stream).delete()
notify_default_streams(realm)
def get_default_streams_for_realm(realm): def get_default_streams_for_realm(realm):
# type: (Realm) -> List[Stream] # type: (Realm) -> List[Stream]
@@ -2049,6 +2060,11 @@ def get_default_subs(user_profile):
# to some day further customize how we set up default streams for new users. # to some day further customize how we set up default streams for new users.
return get_default_streams_for_realm(user_profile.realm) return get_default_streams_for_realm(user_profile.realm)
# returns default streams in json serializeable format
def streams_to_dicts_sorted(streams):
# type: (List[Stream]) -> List[Dict[str, Any]]
return sorted([stream.to_dict() for stream in streams], key=lambda elt: elt["name"])
def do_update_user_activity_interval(user_profile, log_time): def do_update_user_activity_interval(user_profile, log_time):
# type: (UserProfile, datetime.datetime) -> None # type: (UserProfile, datetime.datetime) -> None
effective_end = log_time + datetime.timedelta(minutes=15) effective_end = log_time + datetime.timedelta(minutes=15)
@@ -2687,6 +2703,8 @@ def fetch_initial_state_data(user_profile, event_types, queue_id):
if want('stream'): if want('stream'):
state['streams'] = do_get_streams(user_profile) state['streams'] = do_get_streams(user_profile)
if want('default_streams'):
state['realm_default_streams'] = streams_to_dicts_sorted(get_default_streams_for_realm(user_profile.realm))
if want('update_display_settings'): if want('update_display_settings'):
state['twenty_four_hour_time'] = user_profile.twenty_four_hour_time state['twenty_four_hour_time'] = user_profile.twenty_four_hour_time
@@ -2760,6 +2778,8 @@ def apply_events(state, events, user_profile):
elif event['op'] == "vacate": elif event['op'] == "vacate":
stream_ids = [s["stream_id"] for s in event['streams']] stream_ids = [s["stream_id"] for s in event['streams']]
state['streams'] = [s for s in state['streams'] if s["stream_id"] not in stream_ids] state['streams'] = [s for s in state['streams'] if s["stream_id"] not in stream_ids]
elif event['type'] == 'default_streams':
state['realm_default_streams'] = event['default_streams']
elif event['type'] == 'realm': elif event['type'] == 'realm':
field = 'realm_' + event['property'] field = 'realm_' + event['property']
state[field] = event['value'] state[field] = event['value']
@@ -3167,8 +3187,8 @@ def get_occupied_streams(realm):
return Stream.objects.filter(id__in=stream_ids, realm=realm, deactivated=False) return Stream.objects.filter(id__in=stream_ids, realm=realm, deactivated=False)
def do_get_streams(user_profile, include_public=True, include_subscribed=True, def do_get_streams(user_profile, include_public=True, include_subscribed=True,
include_all_active=False): include_all_active=False, include_default=False):
# type: (UserProfile, bool, bool, bool) -> List[Dict[str, Any]] # type: (UserProfile, bool, bool, bool, bool) -> List[Dict[str, Any]]
if include_all_active and not user_profile.is_api_super_user: if include_all_active and not user_profile.is_api_super_user:
raise JsonableError(_("User not authorized for this query")) raise JsonableError(_("User not authorized for this query"))
@@ -3199,6 +3219,13 @@ def do_get_streams(user_profile, include_public=True, include_subscribed=True,
streams = [(row.to_dict()) for row in query] streams = [(row.to_dict()) for row in query]
streams.sort(key=lambda elt: elt["name"]) streams.sort(key=lambda elt: elt["name"])
if include_default:
is_default = {}
default_streams = get_default_streams_for_realm(user_profile.realm)
for default_stream in default_streams:
is_default[default_stream.id] = True
for stream in streams:
stream['is_default'] = is_default.get(stream["stream_id"], False)
return streams return streams

View File

@@ -31,6 +31,7 @@ from zerver.lib.actions import (
do_remove_realm_filter, do_remove_realm_filter,
do_remove_subscription, do_remove_subscription,
do_rename_stream, do_rename_stream,
do_add_default_stream,
do_set_muted_topics, do_set_muted_topics,
do_set_realm_create_stream_by_admins_only, do_set_realm_create_stream_by_admins_only,
do_set_realm_name, do_set_realm_name,
@@ -377,6 +378,21 @@ class EventsRegisterTest(AuthedTestCase):
error = alert_words_checker('events[0]', events[0]) error = alert_words_checker('events[0]', events[0])
self.assert_on_error(error) self.assert_on_error(error)
def test_default_streams_events(self):
default_streams_checker = check_dict([
('type', equals('default_streams')),
('default_streams', check_list(check_dict([
('description', check_string),
('invite_only', check_bool),
('name', check_string),
('stream_id', check_int),
]))),
])
events = self.do_test(lambda: do_add_default_stream(self.user_profile.realm, "Scotland"))
error = default_streams_checker('events[0]', events[0])
self.assert_on_error(error)
def test_muted_topics_events(self): def test_muted_topics_events(self):
# type: () -> None # type: () -> None
muted_topics_checker = check_dict([ muted_topics_checker = check_dict([
@@ -387,7 +403,6 @@ class EventsRegisterTest(AuthedTestCase):
error = muted_topics_checker('events[0]', events[0]) error = muted_topics_checker('events[0]', events[0])
self.assert_on_error(error) self.assert_on_error(error)
def test_change_full_name(self): def test_change_full_name(self):
# type: () -> None # type: () -> None
schema_checker = check_dict([ schema_checker = check_dict([

View File

@@ -862,6 +862,7 @@ def home(request):
alert_words = register_ret['alert_words'], alert_words = register_ret['alert_words'],
muted_topics = register_ret['muted_topics'], muted_topics = register_ret['muted_topics'],
realm_filters = register_ret['realm_filters'], realm_filters = register_ret['realm_filters'],
realm_default_streams = register_ret['realm_default_streams'],
is_admin = user_profile.is_realm_admin, is_admin = user_profile.is_realm_admin,
can_create_streams = user_profile.can_create_streams(), can_create_streams = user_profile.can_create_streams(),
name_changes_disabled = name_changes_disabled(user_profile.realm), name_changes_disabled = name_changes_disabled(user_profile.realm),

View File

@@ -169,10 +169,16 @@ def json_make_stream_private(request, user_profile, stream_name=REQ()):
@require_realm_admin @require_realm_admin
@has_request_variables @has_request_variables
def update_stream_backend(request, user_profile, stream_name, def update_stream_backend(request, user_profile, stream_name,
description=REQ(validator=check_string, default=None)): description=REQ(validator=check_string, default=None),
# type: (HttpRequest, UserProfile, str, Optional[str]) -> HttpResponse is_default=REQ(validator=check_bool, default=None)):
# type: (HttpRequest, UserProfile, str, Optional[str], Optional[bool]) -> HttpResponse
if description is not None: if description is not None:
do_change_stream_description(user_profile.realm, stream_name, description) do_change_stream_description(user_profile.realm, stream_name, description)
if is_default is not None:
if is_default:
do_add_default_stream(user_profile.realm, stream_name)
else:
do_remove_default_stream(user_profile.realm, stream_name)
return json_success({}) return json_success({})
def list_subscriptions_backend(request, user_profile): def list_subscriptions_backend(request, user_profile):
@@ -416,11 +422,15 @@ def json_get_subscribers(request, user_profile):
def get_streams_backend(request, user_profile, def get_streams_backend(request, user_profile,
include_public=REQ(validator=check_bool, default=True), include_public=REQ(validator=check_bool, default=True),
include_subscribed=REQ(validator=check_bool, default=True), include_subscribed=REQ(validator=check_bool, default=True),
include_all_active=REQ(validator=check_bool, default=False)): include_all_active=REQ(validator=check_bool, default=False),
# type: (HttpRequest, UserProfile, bool, bool, bool) -> HttpResponse include_default=REQ(validator=check_bool, default=False)):
# type: (HttpRequest, UserProfile, bool, bool, bool, bool) -> HttpResponse
streams = do_get_streams(user_profile, include_public, include_subscribed,
include_all_active) streams = do_get_streams(user_profile, include_public=include_public,
include_subscribed=include_subscribed,
include_all_active=include_all_active,
include_default=include_default)
return json_success({"streams": streams}) return json_success({"streams": streams})
@authenticated_json_post_view @authenticated_json_post_view