From 5754e8dd0ff813a5b43297a5d19c2ec3737b137d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 10 Oct 2014 18:21:44 +0200 Subject: [PATCH] FEATURE: auto-close topics based on last post --- .../components/auto-close-form.js.es6 | 49 +++++++++----- .../controllers/edit-topic-auto-close.js.es6 | 36 +++++----- .../javascripts/discourse/lib/utilities.js | 20 ------ .../javascripts/discourse/models/_post.js | 1 - .../javascripts/discourse/models/category.js | 1 + .../javascripts/discourse/models/composer.js | 2 +- .../discourse/models/post_stream.js | 4 +- .../templates/components/auto-close-form.hbs | 18 +++-- .../discourse/templates/modal/auto_close.hbs | 5 +- .../modal/edit-category-settings.hbs | 11 +--- .../stylesheets/common/base/compose.scss | 21 +++++- app/controllers/application_controller.rb | 1 - app/controllers/categories_controller.rb | 10 ++- app/controllers/posts_controller.rb | 3 - app/controllers/topics_controller.rb | 12 +++- app/models/category.rb | 3 +- app/models/topic.rb | 66 ++++++++++++------- app/serializers/category_serializer.rb | 4 +- app/serializers/post_serializer.rb | 14 +++- .../post_stream_serializer_mixin.rb | 1 - app/serializers/topic_view_serializer.rb | 2 + config/locales/client.en.yml | 14 ++-- ...ast_post_and_auto_close_hours_to_topics.rb | 6 ++ ..._close_based_on_last_post_to_categories.rb | 5 ++ lib/post_creator.rb | 7 ++ spec/components/post_creator_spec.rb | 26 +++++++- spec/controllers/topics_controller_spec.rb | 13 ++-- spec/models/topic_spec.rb | 33 ++++------ 28 files changed, 242 insertions(+), 146 deletions(-) create mode 100644 db/migrate/20141008192525_add_auto_close_based_on_last_post_and_auto_close_hours_to_topics.rb create mode 100644 db/migrate/20141008192526_add_auto_close_based_on_last_post_to_categories.rb diff --git a/app/assets/javascripts/discourse/components/auto-close-form.js.es6 b/app/assets/javascripts/discourse/components/auto-close-form.js.es6 index d63a5cc1630..edd56818e4c 100644 --- a/app/assets/javascripts/discourse/components/auto-close-form.js.es6 +++ b/app/assets/javascripts/discourse/components/auto-close-form.js.es6 @@ -1,27 +1,40 @@ export default Ember.Component.extend({ autoCloseValid: false, + limited: false, - label: function() { - return I18n.t( this.get('labelKey') || 'composer.auto_close_label' ); - }.property('labelKey'), + autoCloseUnits: function() { + var key = this.get("limited") ? "composer.auto_close.limited.units" + : "composer.auto_close.all.units"; + return I18n.t(key); + }.property("limited"), - autoCloseChanged: function() { - if( this.get('autoCloseTime') && this.get('autoCloseTime').length > 0 ) { - this.set('autoCloseTime', this.get('autoCloseTime').replace(/[^:\d-\s]/g, '') ); - } - this.set('autoCloseValid', this.isAutoCloseValid()); - }.observes('autoCloseTime'), + autoCloseExamples: function() { + var key = this.get("limited") ? "composer.auto_close.limited.examples" + : "composer.auto_close.all.examples"; + return I18n.t(key); + }.property("limited"), - isAutoCloseValid: function() { - if (this.get('autoCloseTime')) { - var t = this.get('autoCloseTime').trim(); - if (t.match(/^[\d]{4}-[\d]{1,2}-[\d]{1,2} [\d]{1,2}:[\d]{2}/)) { - return moment(t).isAfter(); // In the future - } else { - return (t.match(/^[\d]+$/) || t.match(/^[\d]{1,2}:[\d]{2}$/)) !== null; - } - } else { + _updateAutoCloseValid: function() { + var isValid = this._isAutoCloseValid(this.get("autoCloseTime"), this.get("limited")); + this.set("autoCloseValid", isValid); + }.observes("autoCloseTime", "limited"), + + _isAutoCloseValid: function(autoCloseTime, limited) { + var t = (autoCloseTime || "").trim(); + if (t.length === 0) { + // "empty" is always valid return true; + } else if (limited) { + // only # of hours in limited mode + return t.match(/^(\d+\.)?\d+$/); + } else { + if (t.match(/^\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{2}(\s?[AP]M)?$/i)) { + // timestamp must be in the future + return moment(t).isAfter(); + } else { + // either # of hours or absolute time + return (t.match(/^(\d+\.)?\d+$/) || t.match(/^\d{1,2}:\d{2}(\s?[AP]M)?$/i)) !== null; + } } } }); diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 index d36ae269cd7..b9526f4138e 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 @@ -1,5 +1,4 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; - import ObjectController from 'discourse/controllers/object'; /** @@ -16,24 +15,23 @@ export default ObjectController.extend(ModalFunctionality, { auto_close_invalid: Em.computed.not('auto_close_valid'), setAutoCloseTime: function() { - if( this.get('details.auto_close_at') ) { - var closeTime = new Date( this.get('details.auto_close_at') ); + var autoCloseTime = null; + + if (this.get("details.auto_close_based_on_last_post")) { + autoCloseTime = this.get("details.auto_close_hours"); + } else if (this.get("details.auto_close_at")) { + var closeTime = new Date(this.get("details.auto_close_at")); if (closeTime > new Date()) { - this.set('auto_close_time', moment(closeTime).format("YYYY-MM-DD HH:mm")); + autoCloseTime = moment(closeTime).format("YYYY-MM-DD HH:mm"); } - } else { - this.set('details.auto_close_time', ''); } - }.observes('details.auto_close_at'), + + this.set("auto_close_time", autoCloseTime); + }.observes("details.{auto_close_at,auto_close_hours}"), actions: { - saveAutoClose: function() { - this.setAutoClose( this.get('auto_close_time') ); - }, - - removeAutoClose: function() { - this.setAutoClose(null); - } + saveAutoClose: function() { this.setAutoClose(this.get("auto_close_time")); }, + removeAutoClose: function() { this.setAutoClose(null); } }, setAutoClose: function(time) { @@ -43,16 +41,20 @@ export default ObjectController.extend(ModalFunctionality, { url: '/t/' + this.get('id') + '/autoclose', type: 'PUT', dataType: 'json', - data: { auto_close_time: Discourse.Utilities.timestampFromAutocloseString(time) } + data: { + auto_close_time: time, + auto_close_based_on_last_post: this.get("details.auto_close_based_on_last_post"), + } }).then(function(result){ if (result.success) { self.send('closeModal'); self.set('details.auto_close_at', result.auto_close_at); + self.set('details.auto_close_hours', result.auto_close_hours); } else { - bootbox.alert(I18n.t('composer.auto_close_error'), function() { self.send('showModal'); } ); + bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('showModal'); } ); } }, function () { - bootbox.alert(I18n.t('composer.auto_close_error'), function() { self.send('showModal'); } ); + bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('showModal'); } ); }); } diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index 2de94c45e21..3ee9f2a9817 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -353,26 +353,6 @@ Discourse.Utilities = { } }, - timestampFromAutocloseString: function(arg) { - if (!arg) return null; - if (arg.match(/^[\d]{4}-[\d]{1,2}-[\d]{1,2} [\d]{1,2}:[\d]{2}/)) { - return moment(arg).toJSON(); // moment will add the timezone - } else { - var matches = arg.match(/^([\d]{1,2}):([\d]{2})$/); // just the time HH:MM - if (matches) { - var now = moment(), - t = moment(new Date(now.year(), now.month(), now.date(), matches[1], matches[2])); - if (t.isAfter()) { - return t.toJSON(); - } else { - return t.add('days', 1).toJSON(); - } - } else { - return (arg === '' ? null : arg); - } - } - }, - defaultHomepage: function() { // the homepage is the first item of the 'top_menu' site setting return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0]; diff --git a/app/assets/javascripts/discourse/models/_post.js b/app/assets/javascripts/discourse/models/_post.js index ccc3a93a687..58094963c20 100644 --- a/app/assets/javascripts/discourse/models/_post.js +++ b/app/assets/javascripts/discourse/models/_post.js @@ -163,7 +163,6 @@ Discourse.Post = Discourse.Model.extend({ title: this.get('title'), image_sizes: this.get('imageSizes'), target_usernames: this.get('target_usernames'), - auto_close_time: Discourse.Utilities.timestampFromAutocloseString(this.get('auto_close_time')) }; var metaData = this.get('metaData'); diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js index 0b40ed3e0ab..bc94f89e013 100644 --- a/app/assets/javascripts/discourse/models/category.js +++ b/app/assets/javascripts/discourse/models/category.js @@ -63,6 +63,7 @@ Discourse.Category = Discourse.Model.extend({ secure: this.get('secure'), permissions: this.get('permissionsForUpdate'), auto_close_hours: this.get('auto_close_hours'), + auto_close_based_on_last_post: this.get("auto_close_based_on_last_post"), position: this.get('position'), email_in: this.get('email_in'), email_in_allow_strangers: this.get('email_in_allow_strangers'), diff --git a/app/assets/javascripts/discourse/models/composer.js b/app/assets/javascripts/discourse/models/composer.js index 77d001be512..164e3c604b7 100644 --- a/app/assets/javascripts/discourse/models/composer.js +++ b/app/assets/javascripts/discourse/models/composer.js @@ -521,7 +521,6 @@ Discourse.Composer = Discourse.Model.extend({ admin: currentUser.get('admin'), yours: true, newPost: true, - auto_close_time: Discourse.Utilities.timestampFromAutocloseString(this.get('auto_close_time')) }); if(post) { @@ -562,6 +561,7 @@ Discourse.Composer = Discourse.Model.extend({ // It's no longer a new post createdPost.set('newPost', false); topic.set('draft_sequence', result.draft_sequence); + topic.set('details.auto_close_at', result.topic_auto_close_at); postStream.commitPost(createdPost); addedToStream = true; } else { diff --git a/app/assets/javascripts/discourse/models/post_stream.js b/app/assets/javascripts/discourse/models/post_stream.js index c51b973a6c1..b428e97cf5d 100644 --- a/app/assets/javascripts/discourse/models/post_stream.js +++ b/app/assets/javascripts/discourse/models/post_stream.js @@ -760,6 +760,9 @@ Discourse.PostStream = Em.Object.extend({ return existing; } + // Update the auto_close_at value of the topic + this.set("topic.details.auto_close_at", post.get("topic_auto_close_at")); + post.set('topic', this.get('topic')); postIdentityMap.set(post.get('id'), post); @@ -822,7 +825,6 @@ Discourse.PostStream = Em.Object.extend({ @returns {Promise} a promise that will resolve to the posts in the order requested. **/ loadIntoIdentityMap: function(postIds) { - // If we don't want any posts, return a promise that resolves right away if (Em.isEmpty(postIds)) { return Ember.RSVP.resolve(); diff --git a/app/assets/javascripts/discourse/templates/components/auto-close-form.hbs b/app/assets/javascripts/discourse/templates/components/auto-close-form.hbs index 11479923a7a..c6fe0211cba 100644 --- a/app/assets/javascripts/discourse/templates/components/auto-close-form.hbs +++ b/app/assets/javascripts/discourse/templates/components/auto-close-form.hbs @@ -1,11 +1,19 @@
- - {{label}} - {{text-field value=autoCloseTime}} - {{i18n composer.auto_close_units}} +
- {{i18n composer.auto_close_examples}} + {{autoCloseExamples}} +
+
+
diff --git a/app/assets/javascripts/discourse/templates/modal/auto_close.hbs b/app/assets/javascripts/discourse/templates/modal/auto_close.hbs index e0eaf6b73ac..0c037bfb832 100644 --- a/app/assets/javascripts/discourse/templates/modal/auto_close.hbs +++ b/app/assets/javascripts/discourse/templates/modal/auto_close.hbs @@ -1,6 +1,9 @@