Topic Auto-Close: admins and mods can set a topic to automatically close after a number of days

This commit is contained in:
Neil Lalonde
2013-05-07 14:25:41 -04:00
parent 87469fa4b7
commit 9828c87525
33 changed files with 600 additions and 55 deletions
@@ -8,6 +8,7 @@
**/
Discourse.TopicAdminMenuController = Discourse.ObjectController.extend({
visible: false,
needs: ['modal'],
show: function() {
this.set('visible', true);
@@ -15,6 +16,15 @@ Discourse.TopicAdminMenuController = Discourse.ObjectController.extend({
hide: function() {
this.set('visible', false);
},
autoClose: function() {
var modalController = this.get('controllers.modal');
if (modalController) {
var v = Discourse.EditTopicAutoCloseView.create();
v.set('topic', this.get('content'));
modalController.show(v);
}
}
});
@@ -65,6 +65,11 @@ Discourse.Composer = Discourse.Model.extend({
return false;
}.property('editingPost', 'creatingTopic', 'post.post_number'),
showAdminOptions: function() {
if (this.get('creatingTopic') && Discourse.get('currentUser.staff')) return true;
return false;
}.property('editTitle'),
togglePreview: function() {
this.toggleProperty('showPreview');
Discourse.KeyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
@@ -354,7 +359,8 @@ Discourse.Composer = Discourse.Model.extend({
actions_summary: Em.A(),
moderator: currentUser.get('moderator'),
yours: true,
newPost: true
newPost: true,
auto_close_days: this.get('auto_close_days')
});
// If we're in a topic, we can append the post instantly.
@@ -532,7 +538,13 @@ Discourse.Composer = Discourse.Model.extend({
var reply = this.get('reply') || "";
while (Discourse.BBCode.QUOTE_REGEXP.test(reply)) { reply = reply.replace(Discourse.BBCode.QUOTE_REGEXP, ""); }
return reply.replace(/\s+/img, " ").trim().length;
}.property('reply')
}.property('reply'),
autoCloseChanged: function() {
if( this.get('auto_close_days') && this.get('auto_close_days').length > 0 ) {
this.set('auto_close_days', this.get('auto_close_days').replace(/[^\d]/g, '') )
}
}.observes('auto_close_days')
});
@@ -168,7 +168,8 @@ Discourse.Post = Discourse.Model.extend({
archetype: this.get('archetype'),
title: this.get('title'),
image_sizes: this.get('imageSizes'),
target_usernames: this.get('target_usernames')
target_usernames: this.get('target_usernames'),
auto_close_days: this.get('auto_close_days')
};
// Put the metaData into the request
@@ -23,48 +23,60 @@
{{#if content.viewOpen}}
<div class='control-row reply-area'>
<div class='reply-to'>{{{content.actionTitle}}}:</div>
<div class='reply-to'>{{{content.actionTitle}}}:</div>
{{#if content.editTitle}}
<div class='form-element clearfix'>
{{#if content.creatingPrivateMessage}}
{{view Discourse.UserSelector topicIdBinding="controller.controllers.topic.content.id" excludeCurrentUser="true" id="private-message-users" class="span8" placeholderKey="composer.users_placeholder" tabindex="1" usernamesBinding="content.targetUsernames"}}
{{/if}}
{{view Discourse.TextField valueBinding="content.title" tabindex="2" id="reply-title" maxlength="255" class="span8" placeholderKey="composer.title_placeholder"}}
{{#unless content.creatingPrivateMessage}}
{{view Discourse.ComboboxViewCategory valueAttribute="name" contentBinding="Discourse.site.categories" valueBinding="content.categoryName"}}
{{#if content.archetype.hasOptions}}
<button class='btn' {{action showOptions target="controller"}}>{{i18n topic.options}}</button>
{{/if}}
{{/unless}}
</div>
{{/if}}
<div class='wmd-controls'>
<div class='textarea-wrapper'>
<div class='wmd-button-bar' id='wmd-button-bar'></div>
{{view Discourse.NotifyingTextArea parentBinding="view" tabindex="3" valueBinding="content.reply" id="wmd-input" placeholderKey="composer.reply_placeholder"}}
</div>
<div class='preview-wrapper'>
<div id='wmd-preview' {{bindAttr class="controller.hidePreview:hidden"}}></div>
</div>
{{#if Discourse.currentUser}}
<a href="#" {{action togglePreview target="controller"}} class='toggle-preview'>{{{content.toggleText}}}</a>
<div id='draft-status'></div>
{{#if view.loadingImage}}
<div id="image-uploading">
{{i18n image_selector.uploading_image}} {{view.uploadProgress}}% <a id="cancel-image-upload">{{i18n cancel}}</a>
</div>
{{/if}}
{{#if content.editTitle}}
<div class='form-element clearfix'>
{{#if content.creatingPrivateMessage}}
{{view Discourse.UserSelector topicIdBinding="controller.controllers.topic.content.id" excludeCurrentUser="true" id="private-message-users" class="span8" placeholderKey="composer.users_placeholder" tabindex="1" usernamesBinding="content.targetUsernames"}}
{{/if}}
{{view Discourse.TextField valueBinding="content.title" tabindex="2" id="reply-title" maxlength="255" class="span8" placeholderKey="composer.title_placeholder"}}
{{#unless content.creatingPrivateMessage}}
{{view Discourse.ComboboxViewCategory valueAttribute="name" contentBinding="Discourse.site.categories" valueBinding="content.categoryName"}}
{{#if content.archetype.hasOptions}}
<button class='btn' {{action showOptions target="controller"}}>{{i18n topic.options}}</button>
{{/if}}
{{#if content.showAdminOptions}}
<button class="btn no-text" {{action toggleAdminOptions target="view"}}><i class="icon icon-wrench"></i></button>
{{/if}}
{{/unless}}
</div>
{{#if Discourse.currentUser}}
<div class='submit-panel'>
<button {{action save target="controller"}} tabindex="4" {{bindAttr disabled="content.cantSubmitPost"}} class='btn btn-primary create'>{{view.content.saveText}}</button>
<a href='#' {{action cancel target="controller"}} class='cancel' tabindex="4">{{i18n cancel}}</a>
<div class="admin-options-form">
<div class="auto-close-fields">
<i class="icon icon-time"></i>
{{i18n composer.auto_close_label}}
{{view Discourse.TextField valueBinding="content.auto_close_days" maxlength="5"}}
{{i18n composer.auto_close_units}}
</div>
</div>
{{/if}}
<div class='wmd-controls'>
<div class='textarea-wrapper'>
<div class='wmd-button-bar' id='wmd-button-bar'></div>
{{view Discourse.NotifyingTextArea parentBinding="view" tabindex="3" valueBinding="content.reply" id="wmd-input" placeholderKey="composer.reply_placeholder"}}
</div>
<div class='preview-wrapper'>
<div id='wmd-preview' {{bindAttr class="controller.hidePreview:hidden"}}></div>
</div>
{{#if Discourse.currentUser}}
<a href="#" {{action togglePreview target="controller"}} class='toggle-preview'>{{{content.toggleText}}}</a>
<div id='draft-status'></div>
{{#if view.loadingImage}}
<div id="image-uploading">
{{i18n image_selector.uploading_image}} {{view.uploadProgress}}% <a id="cancel-image-upload">{{i18n cancel}}</a>
</div>
{{/if}}
{{/if}}
</div>
{{#if Discourse.currentUser}}
<div class='submit-panel'>
<button {{action save target="controller"}} tabindex="4" {{bindAttr disabled="content.cantSubmitPost"}} class='btn btn-primary create'>{{view.content.saveText}}</button>
<a href='#' {{action cancel target="controller"}} class='cancel' tabindex="4">{{i18n cancel}}</a>
</div>
{{/if}}
</div>
{{else}}
@@ -0,0 +1,15 @@
<div class="modal-body">
<form>
<div class="auto-close-fields">
<i class="icon icon-time"></i>
{{i18n composer.auto_close_label}}
{{view Discourse.TextField valueBinding="view.auto_close_days" maxlength="5"}}
{{i18n composer.auto_close_units}}
</div>
</form>
</div>
<div class="modal-footer">
<button class='btn btn-primary' {{action saveAutoClose target="view"}} data-dismiss="modal">{{i18n topic.auto_close_save}}</button>
<button class='btn' data-dismiss="modal">{{i18n topic.auto_close_cancel}}</button>
<button class='btn pull-right' {{action removeAutoClose target="view"}} data-dismiss="modal">{{i18n topic.auto_close_remove}}</button>
</div>
@@ -72,6 +72,9 @@
{{/unless}}
{{else}}
{{#if view.fullyLoaded}}
{{view Discourse.TopicClosingView topicBinding="controller.content"}}
{{view Discourse.TopicFooterButtonsView topicBinding="controller.content"}}
{{#if controller.content.suggested_topics.length}}
@@ -18,6 +18,7 @@
<button {{action toggleClosed}} class='btn btn-admin'><i class='icon-unlock'></i> {{i18n topic.actions.open}}</button>
{{else}}
<button {{action toggleClosed}} class='btn btn-admin'><i class='icon-lock'></i> {{i18n topic.actions.close}}</button>
<button {{action autoClose}} class='btn btn-admin'><i class='icon-time'></i> {{i18n topic.actions.auto_close}}</button>
{{/if}}
</li>
@@ -57,5 +58,5 @@
</ul>
</div>
{{else}}
<button class='btn' id='show-topic-admin' {{action show}}><i class='icon icon-wrench'></i></button>
<button class='btn no-text' id='show-topic-admin' {{action show}}><i class='icon icon-wrench'></i></button>
{{/if}}
@@ -354,6 +354,19 @@ Discourse.ComposerView = Discourse.View.extend({
childDidInsertElement: function(e) {
return this.initEditor();
},
toggleAdminOptions: function() {
var $adminOpts = $('.admin-options-form'),
$wmd = $('.wmd-controls'),
wmdTop = parseInt($wmd.css('top'),10);
if( $adminOpts.is(':visible') ) {
$wmd.css('top', wmdTop - parseInt($adminOpts.css('height'),10) + 'px' );
$adminOpts.hide();
} else {
$adminOpts.show();
$wmd.css('top', wmdTop + parseInt($adminOpts.css('height'),10) + 'px' );
}
}
});
@@ -0,0 +1,44 @@
/**
This view handles a modal to set, edit, and remove a topic's auto-close time.
@class EditTopicAutoCloseView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.EditTopicAutoCloseView = Discourse.ModalBodyView.extend({
templateName: 'modal/auto_close',
title: Em.String.i18n('topic.auto_close_title'),
modalClass: 'edit-auto-close-modal',
setDays: function() {
if( this.get('topic.auto_close_at') ) {
var closeTime = Date.create( this.get('topic.auto_close_at') );
if (closeTime.isFuture()) {
this.set('auto_close_days', closeTime.daysSince());
}
}
}.observes('topic'),
saveAutoClose: function() {
this.setAutoClose( parseFloat(this.get('auto_close_days')) );
},
removeAutoClose: function() {
this.setAutoClose(null);
},
setAutoClose: function(days) {
Discourse.ajax({
url: "/t/" + this.get('topic.id') + "/autoclose",
type: 'PUT',
dataType: 'json',
data: { auto_close_days: days > 0 ? days : null }
}).then(function(){
window.location.reload();
}, function (error) {
bootbox.alert(Em.String.i18n('generic_error'));
});
}
});
@@ -0,0 +1,47 @@
/**
This view is used for rendering the notification that a topic will
automatically close.
@class TopicClosingView
@extends Ember.ContainerView
@namespace Discourse
@module Discourse
**/
Discourse.TopicClosingView = Discourse.View.extend({
elementId: 'topic-closing-info',
templateName: 'topic_closing',
render: function(buffer) {
if (!this.present('topic.auto_close_at')) return;
var autoCloseAt = Date.create(this.get('topic.auto_close_at'));
if (autoCloseAt.isPast()) return;
var timeLeftString, reRenderDelay, minutesLeft = autoCloseAt.minutesSince();
if (minutesLeft > 1440) {
timeLeftString = Em.String.i18n('in_n_days', {count: autoCloseAt.daysSince()});
if( minutesLeft > 2160 ) {
reRenderDelay = 12 * 60 * 60000;
} else {
reRenderDelay = 60 * 60000;
}
} else if (minutesLeft > 90) {
timeLeftString = Em.String.i18n('in_n_hours', {count: autoCloseAt.hoursSince()});
reRenderDelay = 30 * 60000;
} else if (minutesLeft > 2) {
timeLeftString = Em.String.i18n('in_n_minutes', {count: autoCloseAt.minutesSince()});
reRenderDelay = 60000;
} else {
timeLeftString = Em.String.i18n('in_n_seconds', {count: autoCloseAt.secondsSince()});
reRenderDelay = 1000;
}
buffer.push('<h3><i class="icon icon-time"></i> ');
buffer.push( Em.String.i18n('topic.auto_close_notice', {timeLeft: timeLeftString}) );
buffer.push('</h3>');
this.rerender.bind(this).delay(reRenderDelay);
}
});
@@ -2,7 +2,7 @@
This view is used for rendering the buttons at the footer of the topic
@class TopicFooterButtonsView
@extends Discourse.View
@extends Ember.ContainerView
@namespace Discourse
@module Discourse
**/