mirror of
https://github.com/zulip/zulip.git
synced 2025-11-08 07:52:19 +00:00
Add realm setting to time-limit editing of message content.
This is controlled through the admin tab and a new field in the Realms table. Notes: * The admin tab setting takes a value in minutes, whereas the backend stores it in seconds. * This setting is unused when allow_message_editing is false. * There is some generosity in how the limit is enforced. For instance, if the user sees the hovering edit button, we ensure they have at least 5 seconds to click it, and if the user gets to the message edit form, we ensure they have at least 10 seconds to make the edit, by relaxing the limit. * This commit also includes a countdown timer in the message edit form. Resolves #903.
This commit is contained in:
@@ -312,7 +312,7 @@ casper.waitForSelector('input[type="checkbox"][id="id_realm_allow_message_editin
|
|||||||
casper.click('input[type="checkbox"][id="id_realm_allow_message_editing"]');
|
casper.click('input[type="checkbox"][id="id_realm_allow_message_editing"]');
|
||||||
casper.click('form.admin-realm-form input.btn');
|
casper.click('form.admin-realm-form input.btn');
|
||||||
casper.waitUntilVisible('#admin-realm-message-editing-status', function () {
|
casper.waitUntilVisible('#admin-realm-message-editing-status', function () {
|
||||||
casper.test.assertSelectorHasText('#admin-realm-message-editing-status', 'Users can now edit the content and topics of all their past messages!');
|
casper.test.assertSelectorHasText('#admin-realm-message-editing-status', 'Users can now edit topics for all their messages, and the content of messages which are less than 10 minutes old.');
|
||||||
casper.test.assertEval(function () {
|
casper.test.assertEval(function () {
|
||||||
return document.querySelector('input[type="checkbox"][id="id_realm_allow_message_editing"]').checked;
|
return document.querySelector('input[type="checkbox"][id="id_realm_allow_message_editing"]').checked;
|
||||||
}, 'Allow message editing Setting re-activated');
|
}, 'Allow message editing Setting re-activated');
|
||||||
@@ -353,6 +353,85 @@ casper.then(function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// go back to admin page
|
||||||
|
casper.then(function () {
|
||||||
|
casper.test.info('Administration page');
|
||||||
|
casper.click('a[href^="#administration"]');
|
||||||
|
casper.test.assertUrlMatch(/^http:\/\/[^\/]+\/#administration/, 'URL suggests we are on administration page');
|
||||||
|
casper.test.assertExists('#administration.tab-pane.active', 'Administration page is active');
|
||||||
|
});
|
||||||
|
|
||||||
|
casper.waitForSelector('form.admin-realm-form input.btn', function () {
|
||||||
|
// deactivate message editing
|
||||||
|
casper.waitForSelector('input[type="checkbox"][id="id_realm_allow_message_editing"]', function () {
|
||||||
|
casper.evaluate(function () {
|
||||||
|
$('input[type="text"][id="id_realm_message_content_edit_limit_minutes"]').val('4');
|
||||||
|
});
|
||||||
|
casper.click('input[type="checkbox"][id="id_realm_allow_message_editing"]');
|
||||||
|
casper.click('form.admin-realm-form input.btn');
|
||||||
|
casper.waitUntilVisible('#admin-realm-message-editing-status', function () {
|
||||||
|
casper.test.assertSelectorHasText('#admin-realm-message-editing-status', 'Users can no longer edit their past messages!');
|
||||||
|
casper.test.assertEval(function () {
|
||||||
|
return !(document.querySelector('input[type="checkbox"][id="id_realm_allow_message_editing"]').checked);
|
||||||
|
}, 'Allow message editing Setting de-activated');
|
||||||
|
casper.test.assertEval(function () {
|
||||||
|
return $('input[type="text"][id="id_realm_message_content_edit_limit_minutes"]').val() === '4';
|
||||||
|
}, 'Message content edit limit now 4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// allow message editing again, and check that the old edit limit is still there
|
||||||
|
casper.waitForSelector('input[type="checkbox"][id="id_realm_allow_message_editing"]', function () {
|
||||||
|
casper.click('input[type="checkbox"][id="id_realm_allow_message_editing"]');
|
||||||
|
casper.click('form.admin-realm-form input.btn');
|
||||||
|
casper.waitUntilVisible('#admin-realm-message-editing-status', function () {
|
||||||
|
casper.test.assertSelectorHasText('#admin-realm-message-editing-status', 'Users can now edit topics for all their messages, and the content of messages which are less than 4 minutes old.');
|
||||||
|
casper.test.assertEval(function () {
|
||||||
|
return document.querySelector('input[type="checkbox"][id="id_realm_allow_message_editing"]').checked;
|
||||||
|
}, 'Allow message editing Setting activated');
|
||||||
|
casper.test.assertEval(function () {
|
||||||
|
return $('input[type="text"][id="id_realm_message_content_edit_limit_minutes"]').val() === '4';
|
||||||
|
}, 'Message content edit limit still 4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// allow arbitrary message editing
|
||||||
|
casper.waitForSelector('input[type="checkbox"][id="id_realm_allow_message_editing"]', function () {
|
||||||
|
casper.evaluate(function () {
|
||||||
|
$('input[type="text"][id="id_realm_message_content_edit_limit_minutes"]').val('0');
|
||||||
|
});
|
||||||
|
casper.click('form.admin-realm-form input.btn');
|
||||||
|
casper.waitUntilVisible('#admin-realm-message-editing-status', function () {
|
||||||
|
casper.test.assertSelectorHasText('#admin-realm-message-editing-status', 'Users can now edit the content and topics of all their past messages!');
|
||||||
|
casper.test.assertEval(function () {
|
||||||
|
return document.querySelector('input[type="checkbox"][id="id_realm_allow_message_editing"]').checked;
|
||||||
|
}, 'Allow message editing Setting still activated');
|
||||||
|
casper.test.assertEval(function () {
|
||||||
|
return $('input[type="text"][id="id_realm_message_content_edit_limit_minutes"]').val() === '0';
|
||||||
|
}, 'Message content edit limit is 0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// disallow message editing, with illegal edit limit value. should be fixed by admin.js
|
||||||
|
casper.waitForSelector('input[type="checkbox"][id="id_realm_allow_message_editing"]', function () {
|
||||||
|
casper.evaluate(function () {
|
||||||
|
$('input[type="text"][id="id_realm_message_content_edit_limit_minutes"]').val('moo');
|
||||||
|
});
|
||||||
|
casper.click('input[type="checkbox"][id="id_realm_allow_message_editing"]');
|
||||||
|
casper.click('form.admin-realm-form input.btn');
|
||||||
|
casper.waitUntilVisible('#admin-realm-message-editing-status', function () {
|
||||||
|
casper.test.assertSelectorHasText('#admin-realm-message-editing-status', 'Users can no longer edit their past messages!');
|
||||||
|
casper.test.assertEval(function () {
|
||||||
|
return !(document.querySelector('input[type="checkbox"][id="id_realm_allow_message_editing"]').checked);
|
||||||
|
}, 'Allow message editing Setting de-activated');
|
||||||
|
casper.test.assertEval(function () {
|
||||||
|
return $('input[type="text"][id="id_realm_message_content_edit_limit_minutes"]').val() === '10';
|
||||||
|
}, 'Message content edit limit has been reset to its default');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
common.then_log_out();
|
common.then_log_out();
|
||||||
|
|
||||||
casper.run(function () {
|
casper.run(function () {
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ function _setup_page() {
|
|||||||
realm_invite_required: page_params.realm_invite_required,
|
realm_invite_required: page_params.realm_invite_required,
|
||||||
realm_invite_by_admins_only: page_params.realm_invite_by_admins_only,
|
realm_invite_by_admins_only: page_params.realm_invite_by_admins_only,
|
||||||
realm_create_stream_by_admins_only: page_params.realm_create_stream_by_admins_only,
|
realm_create_stream_by_admins_only: page_params.realm_create_stream_by_admins_only,
|
||||||
realm_allow_message_editing: page_params.realm_allow_message_editing
|
realm_allow_message_editing: page_params.realm_allow_message_editing,
|
||||||
|
realm_message_content_edit_limit_minutes: Math.ceil(page_params.realm_message_content_edit_limit_seconds / 60)
|
||||||
};
|
};
|
||||||
var admin_tab = templates.render('admin_tab', options);
|
var admin_tab = templates.render('admin_tab', options);
|
||||||
$("#administration").html(admin_tab);
|
$("#administration").html(admin_tab);
|
||||||
@@ -340,6 +341,16 @@ function _setup_page() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#id_realm_allow_message_editing").change(function () {
|
||||||
|
if (this.checked) {
|
||||||
|
$("#id_realm_message_content_edit_limit_minutes").removeAttr("disabled");
|
||||||
|
$("#id_realm_message_content_edit_limit_minutes_label").removeClass("control-label-disabled");
|
||||||
|
} else {
|
||||||
|
$("#id_realm_message_content_edit_limit_minutes").attr("disabled", true);
|
||||||
|
$("#id_realm_message_content_edit_limit_minutes_label").addClass("control-label-disabled");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$(".administration").on("submit", "form.admin-realm-form", function (e) {
|
$(".administration").on("submit", "form.admin-realm-form", function (e) {
|
||||||
var name_status = $("#admin-realm-name-status").expectOne();
|
var name_status = $("#admin-realm-name-status").expectOne();
|
||||||
var restricted_to_domain_status = $("#admin-realm-restricted-to-domain-status").expectOne();
|
var restricted_to_domain_status = $("#admin-realm-restricted-to-domain-status").expectOne();
|
||||||
@@ -363,6 +374,19 @@ function _setup_page() {
|
|||||||
var new_invite_by_admins_only = $("#id_realm_invite_by_admins_only").prop("checked");
|
var new_invite_by_admins_only = $("#id_realm_invite_by_admins_only").prop("checked");
|
||||||
var new_create_stream_by_admins_only = $("#id_realm_create_stream_by_admins_only").prop("checked");
|
var new_create_stream_by_admins_only = $("#id_realm_create_stream_by_admins_only").prop("checked");
|
||||||
var new_allow_message_editing = $("#id_realm_allow_message_editing").prop("checked");
|
var new_allow_message_editing = $("#id_realm_allow_message_editing").prop("checked");
|
||||||
|
var new_message_content_edit_limit_minutes = $("#id_realm_message_content_edit_limit_minutes").val();
|
||||||
|
|
||||||
|
// If allow_message_editing is unchecked, message_content_edit_limit_minutes
|
||||||
|
// is irrelevant. Hence if allow_message_editing is unchecked, and
|
||||||
|
// message_content_edit_limit_minutes is poorly formed, we set the latter to
|
||||||
|
// a default value to prevent the server from returning an error.
|
||||||
|
if (!new_allow_message_editing) {
|
||||||
|
if ((parseInt(new_message_content_edit_limit_minutes, 10).toString() !==
|
||||||
|
new_message_content_edit_limit_minutes) ||
|
||||||
|
new_message_content_edit_limit_minutes < 0) {
|
||||||
|
new_message_content_edit_limit_minutes = 10; // Realm.DEFAULT_MESSAGE_CONTENT_EDIT_LIMIT_SECONDS / 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var url = "/json/realm";
|
var url = "/json/realm";
|
||||||
var data = {
|
var data = {
|
||||||
@@ -371,7 +395,8 @@ function _setup_page() {
|
|||||||
invite_required: JSON.stringify(new_invite),
|
invite_required: JSON.stringify(new_invite),
|
||||||
invite_by_admins_only: JSON.stringify(new_invite_by_admins_only),
|
invite_by_admins_only: JSON.stringify(new_invite_by_admins_only),
|
||||||
create_stream_by_admins_only: JSON.stringify(new_create_stream_by_admins_only),
|
create_stream_by_admins_only: JSON.stringify(new_create_stream_by_admins_only),
|
||||||
allow_message_editing: JSON.stringify(new_allow_message_editing)
|
allow_message_editing: JSON.stringify(new_allow_message_editing),
|
||||||
|
message_content_edit_limit_seconds: JSON.stringify(parseInt(new_message_content_edit_limit_minutes, 10) * 60)
|
||||||
};
|
};
|
||||||
|
|
||||||
channel.patch({
|
channel.patch({
|
||||||
@@ -410,11 +435,23 @@ function _setup_page() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (response_data.allow_message_editing !== undefined) {
|
if (response_data.allow_message_editing !== undefined) {
|
||||||
|
// We expect message_content_edit_limit_seconds was sent in the
|
||||||
|
// response as well
|
||||||
|
var data_message_content_edit_limit_minutes = Math.ceil(response_data.message_content_edit_limit_seconds / 60);
|
||||||
if (response_data.allow_message_editing) {
|
if (response_data.allow_message_editing) {
|
||||||
ui.report_success(i18n.t("Users can now edit the content and topics of all their past messages!"), message_editing_status);
|
if (response_data.message_content_edit_limit_seconds > 0) {
|
||||||
|
ui.report_success(i18n.t("Users can now edit topics for all their messages, and the content of messages which are less than __num_minutes__ minutes old.",
|
||||||
|
{'num_minutes' : data_message_content_edit_limit_minutes}),
|
||||||
|
message_editing_status);
|
||||||
|
} else {
|
||||||
|
ui.report_success(i18n.t("Users can now edit the content and topics of all their past messages!"), message_editing_status);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ui.report_success(i18n.t("Users can no longer edit their past messages!"), message_editing_status);
|
ui.report_success(i18n.t("Users can no longer edit their past messages!"), message_editing_status);
|
||||||
}
|
}
|
||||||
|
// message_content_edit_limit_seconds could have been changed earlier
|
||||||
|
// in this function, so update the field just in case
|
||||||
|
$("#id_realm_message_content_edit_limit_minutes").val(data_message_content_edit_limit_minutes);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function (xhr, error) {
|
error: function (xhr, error) {
|
||||||
|
|||||||
@@ -86,6 +86,17 @@ function handle_edit_keydown(from_topic_edited_only, e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function timer_text(seconds_left) {
|
||||||
|
var minutes = Math.floor(seconds_left / 60);
|
||||||
|
var seconds = seconds_left % 60;
|
||||||
|
if (minutes >= 1) {
|
||||||
|
return i18n.t("__minutes__ min to edit", {'minutes': minutes.toString()});
|
||||||
|
} else if (seconds_left >= 10) {
|
||||||
|
return i18n.t("__seconds__ sec to edit", {'seconds': (seconds - seconds % 5).toString()});
|
||||||
|
}
|
||||||
|
return i18n.t("__seconds__ sec to edit", {'seconds': seconds.toString()});
|
||||||
|
}
|
||||||
|
|
||||||
function edit_message (row, raw_content) {
|
function edit_message (row, raw_content) {
|
||||||
var content_top = row.find('.message_content')[0]
|
var content_top = row.find('.message_content')[0]
|
||||||
.getBoundingClientRect().top;
|
.getBoundingClientRect().top;
|
||||||
@@ -95,7 +106,8 @@ function edit_message (row, raw_content) {
|
|||||||
var form = $(templates.render('message_edit_form',
|
var form = $(templates.render('message_edit_form',
|
||||||
{is_stream: message.is_stream,
|
{is_stream: message.is_stream,
|
||||||
topic: message.subject,
|
topic: message.subject,
|
||||||
content: raw_content}));
|
content: raw_content,
|
||||||
|
minutes_to_edit: Math.floor(page_params.realm_message_content_edit_limit_seconds / 60)}));
|
||||||
|
|
||||||
var edit_obj = {form: form, raw_content: raw_content};
|
var edit_obj = {form: form, raw_content: raw_content};
|
||||||
var original_topic = message.subject;
|
var original_topic = message.subject;
|
||||||
@@ -104,8 +116,69 @@ function edit_message (row, raw_content) {
|
|||||||
|
|
||||||
form.keydown(_.partial(handle_edit_keydown, false));
|
form.keydown(_.partial(handle_edit_keydown, false));
|
||||||
|
|
||||||
|
// We potentially got to this function by clicking a button that implied the
|
||||||
|
// user would be able to edit their message. Give a little bit of buffer in
|
||||||
|
// case the button has been around for a bit, e.g. we show the
|
||||||
|
// edit_content_button (hovering pencil icon) as long as the user would have
|
||||||
|
// been able to click it at the time the mouse entered the message_row. Also
|
||||||
|
// a buffer in case their computer is slow, or stalled for a second, etc
|
||||||
|
// If you change this number also change edit_limit_buffer in
|
||||||
|
// zerver.views.messages.update_message_backend
|
||||||
|
var seconds_left_buffer = 5;
|
||||||
|
|
||||||
|
var now = new XDate();
|
||||||
|
var seconds_left = page_params.realm_message_content_edit_limit_seconds +
|
||||||
|
now.diffSeconds(message.timestamp * 1000);
|
||||||
|
var can_edit_content = (page_params.realm_message_content_edit_limit_seconds === 0) ||
|
||||||
|
(seconds_left + seconds_left_buffer > 0);
|
||||||
|
if (!can_edit_content) {
|
||||||
|
row.find('textarea.message_edit_content').attr("disabled","disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we allow editing at all, give them at least 10 seconds to do it.
|
||||||
|
// If you change this number also change edit_limit_buffer in
|
||||||
|
// zerver.views.messages.update_message_backend
|
||||||
|
var min_seconds_to_edit = 10;
|
||||||
|
seconds_left = Math.floor(Math.max(seconds_left, min_seconds_to_edit));
|
||||||
|
|
||||||
|
// Add a visual timer if appropriate
|
||||||
|
if (can_edit_content && page_params.realm_message_content_edit_limit_seconds > 0) {
|
||||||
|
row.find('.message-edit-timer-control-group').show();
|
||||||
|
$('#message_edit_tooltip').tooltip({ animation: false, placement: 'left',
|
||||||
|
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner message-edit-tooltip-inner"></div></div>'});
|
||||||
|
// I believe these need to be defined outside the countdown_timer, since
|
||||||
|
// row just refers to something like the currently selected message, and
|
||||||
|
// can change out from under us
|
||||||
|
var message_content_row = row.find('textarea.message_edit_content');
|
||||||
|
var message_topic_row, message_topic_propagate_row;
|
||||||
|
if (message.is_stream) {
|
||||||
|
message_topic_row = row.find('input.message_edit_topic');
|
||||||
|
message_topic_propagate_row = row.find('select.message_edit_topic_propagate');
|
||||||
|
}
|
||||||
|
var message_save_row = row.find('button.message_edit_save');
|
||||||
|
var timer_row = row.find('.message_edit_countdown_timer');
|
||||||
|
// Do this right away, rather than waiting for the timer to do its first update,
|
||||||
|
// since otherwise there is a noticeable lag
|
||||||
|
timer_row.text(timer_text(seconds_left));
|
||||||
|
var countdown_timer = setInterval(function () {
|
||||||
|
if (--seconds_left <= 0) {
|
||||||
|
clearInterval(countdown_timer);
|
||||||
|
message_content_row.attr("disabled","disabled");
|
||||||
|
if (message.is_stream) {
|
||||||
|
message_topic_row.attr("disabled","disabled");
|
||||||
|
message_topic_propagate_row.hide();
|
||||||
|
}
|
||||||
|
message_save_row.addClass("disabled");
|
||||||
|
timer_row.text(i18n.t("Time's up!"));
|
||||||
|
} else {
|
||||||
|
timer_row.text(timer_text(seconds_left));
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
currently_editing_messages[message.id] = edit_obj;
|
currently_editing_messages[message.id] = edit_obj;
|
||||||
if (message.type === 'stream' && message.subject === compose.empty_subject_placeholder()) {
|
if ((message.type === 'stream' && message.subject === compose.empty_subject_placeholder()) ||
|
||||||
|
!can_edit_content) {
|
||||||
edit_row.find(".message_edit_topic").focus();
|
edit_row.find(".message_edit_topic").focus();
|
||||||
} else {
|
} else {
|
||||||
edit_row.find(".message_edit_content").focus();
|
edit_row.find(".message_edit_content").focus();
|
||||||
@@ -128,6 +201,7 @@ function edit_message (row, raw_content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composebox_typeahead.initialize_compose_typeahead("#message_edit_content", {emoji: true});
|
composebox_typeahead.initialize_compose_typeahead("#message_edit_content", {emoji: true});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function start_edit_maintaining_scroll(row, content) {
|
function start_edit_maintaining_scroll(row, content) {
|
||||||
|
|||||||
@@ -118,7 +118,12 @@ function message_hover(message_row) {
|
|||||||
message = current_msg_list.get(rows.id(message_row));
|
message = current_msg_list.get(rows.id(message_row));
|
||||||
message_unhover();
|
message_unhover();
|
||||||
message_row.addClass('message_hovered');
|
message_row.addClass('message_hovered');
|
||||||
if (message && message.sent_by_me && !message.status_message && page_params.realm_allow_message_editing) {
|
var now = new XDate();
|
||||||
|
if (message && message.sent_by_me && !message.status_message &&
|
||||||
|
page_params.realm_allow_message_editing &&
|
||||||
|
(page_params.realm_message_content_edit_limit_seconds === 0 ||
|
||||||
|
page_params.realm_message_content_edit_limit_seconds + now.diffSeconds(message.timestamp * 1000) > 0))
|
||||||
|
{
|
||||||
message_row.find('.message_content').find('p:last').append(edit_content_button);
|
message_row.find('.message_content').find('p:last').append(edit_content_button);
|
||||||
}
|
}
|
||||||
current_message_hover = message_row;
|
current_message_hover = message_row;
|
||||||
|
|||||||
@@ -345,6 +345,11 @@ form.admin-realm .control-label {
|
|||||||
color: #d3d3d3;
|
color: #d3d3d3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-realm-message-content-edit-limit-minutes {
|
||||||
|
width: 5ch;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
#settings-status {
|
#settings-status {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ a:hover code {
|
|||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#message_edit_tooltip,
|
||||||
#streams_inline_cog,
|
#streams_inline_cog,
|
||||||
#streams_filter_icon {
|
#streams_filter_icon {
|
||||||
float: right;
|
float: right;
|
||||||
@@ -331,11 +332,19 @@ a:hover code {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#message_edit_tooltip:hover,
|
||||||
#streams_inline_cog:hover,
|
#streams_inline_cog:hover,
|
||||||
#streams_filter_icon:hover {
|
#streams_filter_icon:hover {
|
||||||
opacity: 1.0;
|
opacity: 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-edit-tooltip-inner {
|
||||||
|
width: 200px;
|
||||||
|
position: absolute;
|
||||||
|
right: 7px;
|
||||||
|
top: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
#streams_header a {
|
#streams_header a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -1406,6 +1415,22 @@ div.focused_table {
|
|||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message_edit_countdown_timer {
|
||||||
|
text-align: right;
|
||||||
|
display: inline;
|
||||||
|
color: #a1a1a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message_edit_tooltip {
|
||||||
|
display: inline;
|
||||||
|
color: #a1a1a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-edit-timer-control-group {
|
||||||
|
float: right;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.topic_edit {
|
.topic_edit {
|
||||||
display: none;
|
display: none;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
|||||||
@@ -101,6 +101,21 @@
|
|||||||
{{#if realm_allow_message_editing}}checked="checked"{{/if}} />
|
{{#if realm_allow_message_editing}}checked="checked"{{/if}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="realm_message_content_edit_limit_minutes"
|
||||||
|
id="id_realm_message_content_edit_limit_minutes_label"
|
||||||
|
title="{{t 'If non-zero, users can edit their message for this many minutes after it is sent. If zero, users can edit all their past messages.' }}"
|
||||||
|
class="control-label{{#unless realm_allow_message_editing}} control-label-disabled{{/unless}}">
|
||||||
|
{{t 'Message edit limit in minutes (0 for no limit)' }}
|
||||||
|
</label>
|
||||||
|
<div class="controls">
|
||||||
|
<input type="text" id="id_realm_message_content_edit_limit_minutes"
|
||||||
|
name="realm_message_content_edit_limit_minutes"
|
||||||
|
class="admin-realm-message-content-edit-limit-minutes"
|
||||||
|
value="{{ realm_message_content_edit_limit_minutes }}"
|
||||||
|
{{#unless realm_allow_message_editing}}disabled="disabled"{{/unless}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="controls organization-submission">
|
<div class="controls organization-submission">
|
||||||
<input type="submit" class="btn btn-big btn-primary" value="{{t 'Save changes' }}" />
|
<input type="submit" class="btn btn-big btn-primary" value="{{t 'Save changes' }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="controls edit-controls">
|
<div class="controls edit-controls">
|
||||||
<textarea class="message_edit_content" id="message_edit_content">{{content}}</textarea>
|
<textarea class="message_edit_content" id="message_edit_content">{{content}}</textarea>
|
||||||
@@ -23,6 +24,12 @@
|
|||||||
<div class="controls edit-controls">
|
<div class="controls edit-controls">
|
||||||
<button type="button" class="message_edit_save btn btn-primary btn-small">{{t "Save" }}</button>
|
<button type="button" class="message_edit_save btn btn-primary btn-small">{{t "Save" }}</button>
|
||||||
<button type="button" class="message_edit_cancel btn btn-default btn-small">{{t "Cancel" }}</button>
|
<button type="button" class="message_edit_cancel btn btn-default btn-small">{{t "Cancel" }}</button>
|
||||||
|
<div class="message-edit-timer-control-group">
|
||||||
|
<span class="message_edit_countdown_timer"></span>
|
||||||
|
<span><i id="message_edit_tooltip" class="message_edit_tooltip icon-vector-question-sign" data-toggle="tooltip"
|
||||||
|
title="Message content can only be edited for {{minutes_to_edit}} minutes after it is sent."></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-error edit_error hide"></div>
|
<div class="alert alert-error edit_error hide"></div>
|
||||||
|
|||||||
@@ -412,15 +412,17 @@ def do_set_realm_create_stream_by_admins_only(realm, create_stream_by_admins_onl
|
|||||||
)
|
)
|
||||||
send_event(event, active_user_ids(realm))
|
send_event(event, active_user_ids(realm))
|
||||||
|
|
||||||
def do_set_realm_message_editing(realm, allow_message_editing):
|
def do_set_realm_message_editing(realm, allow_message_editing, message_content_edit_limit_seconds):
|
||||||
# type: (Realm, bool) -> None
|
# type: (Realm, bool, int) -> None
|
||||||
realm.allow_message_editing = allow_message_editing
|
realm.allow_message_editing = allow_message_editing
|
||||||
realm.save(update_fields=['allow_message_editing'])
|
realm.message_content_edit_limit_seconds = message_content_edit_limit_seconds
|
||||||
|
realm.save(update_fields=['allow_message_editing', 'message_content_edit_limit_seconds'])
|
||||||
event = dict(
|
event = dict(
|
||||||
type="realm",
|
type="realm",
|
||||||
op="update_dict",
|
op="update_dict",
|
||||||
property="default",
|
property="default",
|
||||||
data=dict(allow_message_editing=allow_message_editing),
|
data=dict(allow_message_editing=allow_message_editing,
|
||||||
|
message_content_edit_limit_seconds=message_content_edit_limit_seconds),
|
||||||
)
|
)
|
||||||
send_event(event, active_user_ids(realm))
|
send_event(event, active_user_ids(realm))
|
||||||
|
|
||||||
@@ -2388,7 +2390,7 @@ def do_update_message(user_profile, message_id, subject, propagate_mode, content
|
|||||||
edit_history_event = {}
|
edit_history_event = {}
|
||||||
changed_messages = [message]
|
changed_messages = [message]
|
||||||
|
|
||||||
# You can only edit a message if:
|
# You only have permission to edit a message if:
|
||||||
# 1. You sent it, OR:
|
# 1. You sent it, OR:
|
||||||
# 2. This is a topic-only edit for a (no topic) message, OR:
|
# 2. This is a topic-only edit for a (no topic) message, OR:
|
||||||
# 3. This is a topic-only edit and you are an admin.
|
# 3. This is a topic-only edit and you are an admin.
|
||||||
@@ -2400,6 +2402,20 @@ def do_update_message(user_profile, message_id, subject, propagate_mode, content
|
|||||||
else:
|
else:
|
||||||
raise JsonableError(_("You don't have permission to edit this message"))
|
raise JsonableError(_("You don't have permission to edit this message"))
|
||||||
|
|
||||||
|
# We already check for realm.allow_message_editing in
|
||||||
|
# zerver.views.messages.update_message_backend
|
||||||
|
# If there is a change to the content, we also need to check it hasn't been
|
||||||
|
# too long
|
||||||
|
|
||||||
|
# Allow an extra 20 seconds since we potentially allow editing 15 seconds
|
||||||
|
# past the limit, and in case there are network issues, etc. The 15 comes
|
||||||
|
# from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if
|
||||||
|
# you change this value also change those two parameters in message_edit.js.
|
||||||
|
edit_limit_buffer = 20
|
||||||
|
if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0 and \
|
||||||
|
(now() - message.pub_date) > datetime.timedelta(seconds=user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer):
|
||||||
|
raise JsonableError(_("The time limit for editing this message has past"))
|
||||||
|
|
||||||
# Set first_rendered_content to be the oldest version of the
|
# Set first_rendered_content to be the oldest version of the
|
||||||
# rendered content recorded; which is the current version if the
|
# rendered content recorded; which is the current version if the
|
||||||
# content hasn't been edited before. Note that because one could
|
# content hasn't been edited before. Note that because one could
|
||||||
@@ -2708,6 +2724,7 @@ def fetch_initial_state_data(user_profile, event_types, queue_id):
|
|||||||
state['realm_invite_by_admins_only'] = user_profile.realm.invite_by_admins_only
|
state['realm_invite_by_admins_only'] = user_profile.realm.invite_by_admins_only
|
||||||
state['realm_create_stream_by_admins_only'] = user_profile.realm.create_stream_by_admins_only
|
state['realm_create_stream_by_admins_only'] = user_profile.realm.create_stream_by_admins_only
|
||||||
state['realm_allow_message_editing'] = user_profile.realm.allow_message_editing
|
state['realm_allow_message_editing'] = user_profile.realm.allow_message_editing
|
||||||
|
state['realm_message_content_edit_limit_seconds'] = user_profile.realm.message_content_edit_limit_seconds
|
||||||
|
|
||||||
if want('realm_domain'):
|
if want('realm_domain'):
|
||||||
state['realm_domain'] = user_profile.realm.domain
|
state['realm_domain'] = user_profile.realm.domain
|
||||||
|
|||||||
19
zerver/migrations/0025_realm_message_content_edit_limit.py
Normal file
19
zerver/migrations/0025_realm_message_content_edit_limit.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('zerver', '0024_realm_allow_message_editing'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='realm',
|
||||||
|
name='message_content_edit_limit_seconds',
|
||||||
|
field=models.IntegerField(default=600),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -143,7 +143,10 @@ class Realm(ModelReprMixin, models.Model):
|
|||||||
mandatory_topics = models.BooleanField(default=False) # type: bool
|
mandatory_topics = models.BooleanField(default=False) # type: bool
|
||||||
show_digest_email = models.BooleanField(default=True) # type: bool
|
show_digest_email = models.BooleanField(default=True) # type: bool
|
||||||
name_changes_disabled = models.BooleanField(default=False) # type: bool
|
name_changes_disabled = models.BooleanField(default=False) # type: bool
|
||||||
|
|
||||||
allow_message_editing = models.BooleanField(default=True) # type: bool
|
allow_message_editing = models.BooleanField(default=True) # type: bool
|
||||||
|
DEFAULT_MESSAGE_CONTENT_EDIT_LIMIT_SECONDS = 600 # if changed, also change in admin.js
|
||||||
|
message_content_edit_limit_seconds = models.IntegerField(default=DEFAULT_MESSAGE_CONTENT_EDIT_LIMIT_SECONDS) # type: int
|
||||||
|
|
||||||
date_created = models.DateTimeField(default=timezone.now) # type: datetime.datetime
|
date_created = models.DateTimeField(default=timezone.now) # type: datetime.datetime
|
||||||
notifications_stream = models.ForeignKey('Stream', related_name='+', null=True, blank=True) # type: Optional[Stream]
|
notifications_stream = models.ForeignKey('Stream', related_name='+', null=True, blank=True) # type: Optional[Stream]
|
||||||
|
|||||||
@@ -512,12 +512,16 @@ class EventsRegisterTest(AuthedTestCase):
|
|||||||
('type', equals('realm')),
|
('type', equals('realm')),
|
||||||
('op', equals('update_dict')),
|
('op', equals('update_dict')),
|
||||||
('property', equals('default')),
|
('property', equals('default')),
|
||||||
('data', check_dict([('allow_message_editing', check_bool)])),
|
('data', check_dict([('allow_message_editing', check_bool),
|
||||||
|
('message_content_edit_limit_seconds', check_int)])),
|
||||||
])
|
])
|
||||||
# The first False is probably a noop, then we get transitions in both directions.
|
# Test every transition among the four possibilities {T,F} x {0, non-0}
|
||||||
for allow_message_editing in [False, True, False]:
|
for (allow_message_editing, message_content_edit_limit_seconds) in \
|
||||||
|
((True, 0), (False, 0), (True, 0), (False, 1234), (True, 0), (True, 1234), (True, 0),
|
||||||
|
(False, 0), (False, 1234), (False, 0), (True, 1234), (False, 0),
|
||||||
|
(True, 1234), (True, 600), (False, 600), (False, 1234), (True, 600)):
|
||||||
events = self.do_test(lambda: do_set_realm_message_editing(self.user_profile.realm,
|
events = self.do_test(lambda: do_set_realm_message_editing(self.user_profile.realm,
|
||||||
allow_message_editing))
|
allow_message_editing, message_content_edit_limit_seconds))
|
||||||
error = schema_checker('events[0]', events[0])
|
error = schema_checker('events[0]', events[0])
|
||||||
self.assert_on_error(error)
|
self.assert_on_error(error)
|
||||||
|
|
||||||
|
|||||||
@@ -779,6 +779,72 @@ class EditMessageTest(AuthedTestCase):
|
|||||||
})
|
})
|
||||||
self.assert_json_error(result, "Content can't be empty")
|
self.assert_json_error(result, "Content can't be empty")
|
||||||
|
|
||||||
|
def test_edit_message_content_limit(self):
|
||||||
|
def set_message_editing_params(allow_message_editing,
|
||||||
|
message_content_edit_limit_seconds):
|
||||||
|
result = self.client_patch("/json/realm", {
|
||||||
|
'allow_message_editing': ujson.dumps(allow_message_editing),
|
||||||
|
'message_content_edit_limit_seconds': message_content_edit_limit_seconds
|
||||||
|
})
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
def do_edit_message_assert_success(id_, unique_str, topic_only = False):
|
||||||
|
new_subject = 'subject' + unique_str
|
||||||
|
new_content = 'content' + unique_str
|
||||||
|
params_dict = { 'message_id': id_, 'subject': new_subject }
|
||||||
|
if not topic_only:
|
||||||
|
params_dict['content'] = new_content
|
||||||
|
result = self.client.post("/json/update_message", params_dict)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
if topic_only:
|
||||||
|
self.check_message(id_, subject=new_subject)
|
||||||
|
else:
|
||||||
|
self.check_message(id_, subject=new_subject, content=new_content)
|
||||||
|
|
||||||
|
def do_edit_message_assert_error(id_, unique_str, error, topic_only = False):
|
||||||
|
message = Message.objects.get(id=id_)
|
||||||
|
old_subject = message.subject
|
||||||
|
old_content = message.content
|
||||||
|
new_subject = 'subject' + unique_str
|
||||||
|
new_content = 'content' + unique_str
|
||||||
|
params_dict = { 'message_id': id_, 'subject': new_subject }
|
||||||
|
if not topic_only:
|
||||||
|
params_dict['content'] = new_content
|
||||||
|
result = self.client.post("/json/update_message", params_dict)
|
||||||
|
message = Message.objects.get(id=id_)
|
||||||
|
self.assert_json_error(result, error)
|
||||||
|
self.check_message(id_, subject=old_subject, content=old_content)
|
||||||
|
|
||||||
|
self.login("iago@zulip.com")
|
||||||
|
# send a message in the past
|
||||||
|
id_ = self.send_message("iago@zulip.com", "Scotland", Recipient.STREAM,
|
||||||
|
content="content", subject="subject")
|
||||||
|
message = Message.objects.get(id=id_)
|
||||||
|
message.pub_date = message.pub_date - datetime.timedelta(seconds=180)
|
||||||
|
message.save()
|
||||||
|
|
||||||
|
# test the various possible message editing settings
|
||||||
|
# high enough time limit, all edits allowed
|
||||||
|
set_message_editing_params(True, 240)
|
||||||
|
do_edit_message_assert_success(id_, 'A')
|
||||||
|
|
||||||
|
# out of time, only topic editing allowed
|
||||||
|
set_message_editing_params(True, 120)
|
||||||
|
do_edit_message_assert_success(id_, 'B', True)
|
||||||
|
do_edit_message_assert_error(id_, 'C', "The time limit for editing this message has past")
|
||||||
|
|
||||||
|
# infinite time, all edits allowed
|
||||||
|
set_message_editing_params(True, 0)
|
||||||
|
do_edit_message_assert_success(id_, 'D')
|
||||||
|
|
||||||
|
# without allow_message_editing, nothing is allowed
|
||||||
|
set_message_editing_params(False, 240)
|
||||||
|
do_edit_message_assert_error(id_, 'E', "Your organization has turned off message editing.", True)
|
||||||
|
set_message_editing_params(False, 120)
|
||||||
|
do_edit_message_assert_error(id_, 'F', "Your organization has turned off message editing.", True)
|
||||||
|
set_message_editing_params(False, 0)
|
||||||
|
do_edit_message_assert_error(id_, 'G', "Your organization has turned off message editing.", True)
|
||||||
|
|
||||||
def test_propagate_topic_forward(self):
|
def test_propagate_topic_forward(self):
|
||||||
self.login("hamlet@zulip.com")
|
self.login("hamlet@zulip.com")
|
||||||
id1 = self.send_message("hamlet@zulip.com", "Scotland", Recipient.STREAM,
|
id1 = self.send_message("hamlet@zulip.com", "Scotland", Recipient.STREAM,
|
||||||
@@ -982,4 +1048,3 @@ class CheckMessageTest(AuthedTestCase):
|
|||||||
new_count = message_stream_count(parent)
|
new_count = message_stream_count(parent)
|
||||||
self.assertEqual(new_count, old_count + 1)
|
self.assertEqual(new_count, old_count + 1)
|
||||||
self.assertEqual(ret['message'].sender.email, 'othello-bot@zulip.com')
|
self.assertEqual(ret['message'].sender.email, 'othello-bot@zulip.com')
|
||||||
|
|
||||||
|
|||||||
@@ -946,6 +946,7 @@ def home(request):
|
|||||||
realm_invite_by_admins_only = register_ret['realm_invite_by_admins_only'],
|
realm_invite_by_admins_only = register_ret['realm_invite_by_admins_only'],
|
||||||
realm_create_stream_by_admins_only = register_ret['realm_create_stream_by_admins_only'],
|
realm_create_stream_by_admins_only = register_ret['realm_create_stream_by_admins_only'],
|
||||||
realm_allow_message_editing = register_ret['realm_allow_message_editing'],
|
realm_allow_message_editing = register_ret['realm_allow_message_editing'],
|
||||||
|
realm_message_content_edit_limit_seconds = register_ret['realm_message_content_edit_limit_seconds'],
|
||||||
realm_restricted_to_domain = register_ret['realm_restricted_to_domain'],
|
realm_restricted_to_domain = register_ret['realm_restricted_to_domain'],
|
||||||
enter_sends = user_profile.enter_sends,
|
enter_sends = user_profile.enter_sends,
|
||||||
left_side_userlist = register_ret['left_side_userlist'],
|
left_side_userlist = register_ret['left_side_userlist'],
|
||||||
@@ -1103,8 +1104,9 @@ def update_realm(request, user_profile, name=REQ(validator=check_string, default
|
|||||||
invite_required=REQ(validator=check_bool, default=None),
|
invite_required=REQ(validator=check_bool, default=None),
|
||||||
invite_by_admins_only=REQ(validator=check_bool, default=None),
|
invite_by_admins_only=REQ(validator=check_bool, default=None),
|
||||||
create_stream_by_admins_only=REQ(validator=check_bool, default=None),
|
create_stream_by_admins_only=REQ(validator=check_bool, default=None),
|
||||||
allow_message_editing=REQ(validator=check_bool, default=None)):
|
allow_message_editing=REQ(validator=check_bool, default=None),
|
||||||
# type: (HttpRequest, UserProfile, Optional[str], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool]) -> HttpResponse
|
message_content_edit_limit_seconds=REQ(converter=to_non_negative_int, default=None)):
|
||||||
|
# type: (HttpRequest, UserProfile, Optional[str], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[int]) -> HttpResponse
|
||||||
realm = user_profile.realm
|
realm = user_profile.realm
|
||||||
data = {} # type: Dict[str, Any]
|
data = {} # type: Dict[str, Any]
|
||||||
if name is not None and realm.name != name:
|
if name is not None and realm.name != name:
|
||||||
@@ -1122,9 +1124,15 @@ def update_realm(request, user_profile, name=REQ(validator=check_string, default
|
|||||||
if create_stream_by_admins_only is not None and realm.create_stream_by_admins_only != create_stream_by_admins_only:
|
if create_stream_by_admins_only is not None and realm.create_stream_by_admins_only != create_stream_by_admins_only:
|
||||||
do_set_realm_create_stream_by_admins_only(realm, create_stream_by_admins_only)
|
do_set_realm_create_stream_by_admins_only(realm, create_stream_by_admins_only)
|
||||||
data['create_stream_by_admins_only'] = create_stream_by_admins_only
|
data['create_stream_by_admins_only'] = create_stream_by_admins_only
|
||||||
if allow_message_editing is not None and realm.allow_message_editing != allow_message_editing:
|
if (allow_message_editing is not None and realm.allow_message_editing != allow_message_editing) or \
|
||||||
do_set_realm_message_editing(realm, allow_message_editing)
|
(message_content_edit_limit_seconds is not None and realm.message_content_edit_limit_seconds != message_content_edit_limit_seconds):
|
||||||
|
if allow_message_editing is None:
|
||||||
|
allow_message_editing = realm.allow_message_editing
|
||||||
|
if message_content_edit_limit_seconds is None:
|
||||||
|
message_content_edit_limit_seconds = realm.message_content_edit_limit_seconds
|
||||||
|
do_set_realm_message_editing(realm, allow_message_editing, message_content_edit_limit_seconds)
|
||||||
data['allow_message_editing'] = allow_message_editing
|
data['allow_message_editing'] = allow_message_editing
|
||||||
|
data['message_content_edit_limit_seconds'] = message_content_edit_limit_seconds
|
||||||
return json_success(data)
|
return json_success(data)
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
|||||||
Reference in New Issue
Block a user