From 49bc7692bf0f9cfc4eaa10d579649764f47d38e6 Mon Sep 17 00:00:00 2001 From: David Celis Date: Thu, 6 Feb 2014 11:15:00 -0800 Subject: [PATCH 01/27] Include binstubs generated by Rails 4 Commit b516ecc added `bin/` to the .gitignore file. Now that Discourse runs using Rails 4 by default, however, we should include the binstubs generated by `rake rails:update:bin` in version control as this is the recommendation of the Rails core team. Additionally, for those wishing to deploy Discourse to Heroku, these binstubs are actually mandatory according to [this article](https://devcenter.heroku.com/articles/rails4). Other binstubs can continue to be ignored. Signed-off-by: David Celis --- bin/bundle | 3 +++ bin/rails | 4 ++++ bin/rake | 4 ++++ 3 files changed, 11 insertions(+) create mode 100755 bin/bundle create mode 100755 bin/rails create mode 100755 bin/rake diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 00000000000..66e9889e8b4 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/rails b/bin/rails new file mode 100755 index 00000000000..728cd85aa58 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../../config/application', __FILE__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100755 index 00000000000..17240489f64 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative '../config/boot' +require 'rake' +Rake.application.run From 56e09802fea96268218987105edc237f592867d5 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 9 Feb 2014 07:29:16 +1100 Subject: [PATCH 02/27] BUGFIX: Use email sender --- app/jobs/regular/notify_mailing_list_subscribers.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/jobs/regular/notify_mailing_list_subscribers.rb b/app/jobs/regular/notify_mailing_list_subscribers.rb index b344c6360be..b51c250ae04 100644 --- a/app/jobs/regular/notify_mailing_list_subscribers.rb +++ b/app/jobs/regular/notify_mailing_list_subscribers.rb @@ -30,7 +30,8 @@ module Jobs )', post.topic.category_id, CategoryUser.notification_levels[:muted]) .each do |user| if Guardian.new(user).can_see?(post) - UserNotifications.mailing_list_notify(user, post).deliver + message = UserNotifications.mailing_list_notify(user, post) + Email::Sender.new(message, :mailing_list, user).send end end From 90ae4c62245571528676f9697e23688e011747c5 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 9 Feb 2014 07:55:11 +1100 Subject: [PATCH 03/27] Try to fix random failing spec --- spec/components/scheduler/manager_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/components/scheduler/manager_spec.rb b/spec/components/scheduler/manager_spec.rb index 0d6f9f3b921..2fab9349f85 100644 --- a/spec/components/scheduler/manager_spec.rb +++ b/spec/components/scheduler/manager_spec.rb @@ -46,7 +46,7 @@ describe Scheduler::Manager do (0..5).map do Thread.new do - manager = Scheduler::Manager.new(Redis.new) + manager = Scheduler::Manager.new(DiscourseRedis.new) manager.blocking_tick end end.map(&:join) From 862faf2a17fce64a6bf84b1cc9c496de9a69968b Mon Sep 17 00:00:00 2001 From: Jonathan Allard Date: Thu, 6 Feb 2014 00:22:53 -0500 Subject: [PATCH 04/27] Locales/fr: Add filter strings and change :read_more_MF --- config/locales/client.fr.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index f0ee7401337..56f82b0c3e4 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -656,7 +656,23 @@ fr: read_more: "Vous voulez en lire plus? {{catLink}} or {{latestLink}}." # keys ending with _MF use message format, see /spec/components/js_local_helper_spec.rb for samples - read_more_MF: "Il y a { UNREAD, plural, =0 {} one { is 1 unread } other { are # unread } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {is } other{}} 1 new topic} other { {BOTH, select, true{and } false {are } other{}} # new topics} } remaining, or {CATEGORY, select, true {browse other topics in {catLink}} false {{latestLink}} other {}}." + read_more_MF: "Il y a { + UNREAD, plural, + =0 {} + one { + 1 discussion non-lue + } other { + # discussions non-lues + } + } { + NEW, plural, + =0 {} + one { + {BOTH, select, true{et } false {} other{}} 1 nouvelle discussion + } other { + {BOTH, select, true{et } false {} other{}} # nouvelles discussions + } + } à lire, ou {CATEGORY, select, true {allez voir les nouvelles discussions dans {catLink}} false {{latestLink}} other {}}." browse_all_categories: Voir toutes les catégories @@ -1056,6 +1072,8 @@ fr: categories_list: "Liste des Catégories" filters: + with_topics: "Discussions %{filter}" + with_category: "Discussions %{filter} dans %{category}" latest: title: "Récentes" help: "discussions récentes" From 7acef17a352140c8ebb8e13890bd3ef69d853b7d Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 9 Feb 2014 11:10:14 -0500 Subject: [PATCH 05/27] Update Ember for CVE-2014-0046. --- .../assets/javascripts/development/ember.js | 186 +++++++++--------- vendor/assets/javascripts/production/ember.js | 184 ++++++++--------- 2 files changed, 189 insertions(+), 181 deletions(-) diff --git a/vendor/assets/javascripts/development/ember.js b/vendor/assets/javascripts/development/ember.js index 0e3d5ede3d5..73543891a87 100755 --- a/vendor/assets/javascripts/development/ember.js +++ b/vendor/assets/javascripts/development/ember.js @@ -5,7 +5,7 @@ * Portions Copyright 2008-2011 Apple Inc. All rights reserved. * @license Licensed under MIT license * See https://raw.github.com/emberjs/ember.js/master/LICENSE - * @version 1.3.2+pre.773be0ec + * @version 1.3.2 */ @@ -203,7 +203,7 @@ if (!Ember.testing) { * Portions Copyright 2008-2011 Apple Inc. All rights reserved. * @license Licensed under MIT license * See https://raw.github.com/emberjs/ember.js/master/LICENSE - * @version 1.3.2+pre.773be0ec + * @version 1.3.2 */ @@ -286,7 +286,7 @@ var define, requireModule, require, requirejs; @class Ember @static - @version 1.3.2+pre.773be0ec + @version 1.3.2 */ if ('undefined' === typeof Ember) { @@ -313,10 +313,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.3.2+pre.773be0ec' + @default '1.3.2' @static */ -Ember.VERSION = '1.3.2+pre.773be0ec'; +Ember.VERSION = '1.3.2'; /** Standard environmental variables. You can define these in a global `EmberENV` @@ -1029,7 +1029,7 @@ Ember.guidFor = function guidFor(obj) { // META // -var META_DESC = Ember.META_DESC = { +var META_DESC = { writable: true, configurable: false, enumerable: false, @@ -2505,7 +2505,7 @@ ObserverSet.prototype.clear = function() { (function() { -var META_KEY = Ember.META_KEY, +var metaFor = Ember.meta, guidFor = Ember.guidFor, tryFinally = Ember.tryFinally, sendEvent = Ember.sendEvent, @@ -2536,10 +2536,10 @@ var META_KEY = Ember.META_KEY, @return {void} */ function propertyWillChange(obj, keyName) { - var m = obj[META_KEY], - watching = (m && m.watching[keyName] > 0) || keyName === 'length', - proto = m && m.proto, - desc = m && m.descs[keyName]; + var m = metaFor(obj, false), + watching = m.watching[keyName] > 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; if (!watching) { return; } if (proto === obj) { return; } @@ -2566,10 +2566,10 @@ Ember.propertyWillChange = propertyWillChange; @return {void} */ function propertyDidChange(obj, keyName) { - var m = obj[META_KEY], - watching = (m && m.watching[keyName] > 0) || keyName === 'length', - proto = m && m.proto, - desc = m && m.descs[keyName]; + var m = metaFor(obj, false), + watching = m.watching[keyName] > 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; if (proto === obj) { return; } @@ -2642,7 +2642,7 @@ function chainsWillChange(obj, keyName, m) { } function chainsDidChange(obj, keyName, m, suppressEvents) { - if (!(m && m.hasOwnProperty('chainWatchers') && + if (!(m.hasOwnProperty('chainWatchers') && m.chainWatchers[keyName])) { return; } @@ -3645,11 +3645,11 @@ var metaFor = Ember.meta, // utils.js MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, o_defineProperty = Ember.platform.defineProperty; -Ember.watchKey = function(obj, keyName, meta) { +Ember.watchKey = function(obj, keyName) { // can't watch length on Array - it is special... if (keyName === 'length' && typeOf(obj) === 'array') { return; } - var m = meta || metaFor(obj), watching = m.watching; + var m = metaFor(obj), watching = m.watching; // activate watching first time if (!watching[keyName]) { @@ -3674,8 +3674,8 @@ Ember.watchKey = function(obj, keyName, meta) { }; -Ember.unwatchKey = function(obj, keyName, meta) { - var m = meta || metaFor(obj), watching = m.watching; +Ember.unwatchKey = function(obj, keyName) { + var m = metaFor(obj), watching = m.watching; if (watching[keyName] === 1) { watching[keyName] = 0; @@ -3718,8 +3718,7 @@ var metaFor = Ember.meta, // utils.js warn = Ember.warn, watchKey = Ember.watchKey, unwatchKey = Ember.unwatchKey, - FIRST_KEY = /^([^\.\*]+)/, - META_KEY = Ember.META_KEY; + FIRST_KEY = /^([^\.\*]+)/; function firstKey(path) { return path.match(FIRST_KEY)[0]; @@ -3753,24 +3752,24 @@ function addChainWatcher(obj, keyName, node) { if (!nodes[keyName]) { nodes[keyName] = []; } nodes[keyName].push(node); - watchKey(obj, keyName, m); + watchKey(obj, keyName); } var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) { if (!obj || 'object' !== typeof obj) { return; } // nothing to do - var m = obj[META_KEY]; - if (m && !m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + var m = metaFor(obj, false); + if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - var nodes = m && m.chainWatchers; + var nodes = m.chainWatchers; - if (nodes && nodes[keyName]) { + if (nodes[keyName]) { nodes = nodes[keyName]; for (var i = 0, l = nodes.length; i < l; i++) { if (nodes[i] === node) { nodes.splice(i, 1); } } } - unwatchKey(obj, keyName, m); + unwatchKey(obj, keyName); }; // A ChainNode watches a single key on an object. If you provide a starting @@ -3810,14 +3809,14 @@ var ChainNodePrototype = ChainNode.prototype; function lazyGet(obj, key) { if (!obj) return undefined; - var meta = obj[META_KEY]; + var meta = metaFor(obj, false); // check if object meant only to be a prototype - if (meta && meta.proto === obj) return undefined; + if (meta.proto === obj) return undefined; if (key === "@each") return get(obj, key); // if a CP only return cached value - var desc = meta && meta.descs[key]; + var desc = meta.descs[key]; if (desc && desc._cacheable) { if (key in meta.cache) { return meta.cache[key]; @@ -4029,14 +4028,12 @@ ChainNodePrototype.didChange = function(events) { }; Ember.finishChains = function(obj) { - // We only create meta if we really have to - var m = obj[META_KEY], chains = m && m.chains; + var m = metaFor(obj, false), chains = m.chains; if (chains) { if (chains.value() !== obj) { - metaFor(obj).chains = chains = chains.copy(obj); - } else { - chains.didChange(null); + m.chains = chains = chains.copy(obj); } + chains.didChange(null); } }; @@ -4058,8 +4055,8 @@ var metaFor = Ember.meta, // utils.js // get the chains for the current object. If the current object has // chains inherited from the proto they will be cloned and reconfigured for // the current object. -function chainsFor(obj, meta) { - var m = meta || metaFor(obj), ret = m.chains; +function chainsFor(obj) { + var m = metaFor(obj), ret = m.chains; if (!ret) { ret = m.chains = new ChainNode(null, null, obj); } else if (ret.value() !== obj) { @@ -4068,26 +4065,26 @@ function chainsFor(obj, meta) { return ret; } -Ember.watchPath = function(obj, keyPath, meta) { +Ember.watchPath = function(obj, keyPath) { // can't watch length on Array - it is special... if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - var m = meta || metaFor(obj), watching = m.watching; + var m = metaFor(obj), watching = m.watching; if (!watching[keyPath]) { // activate watching first time watching[keyPath] = 1; - chainsFor(obj, m).add(keyPath); + chainsFor(obj).add(keyPath); } else { watching[keyPath] = (watching[keyPath] || 0) + 1; } }; -Ember.unwatchPath = function(obj, keyPath, meta) { - var m = meta || metaFor(obj), watching = m.watching; +Ember.unwatchPath = function(obj, keyPath) { + var m = metaFor(obj), watching = m.watching; if (watching[keyPath] === 1) { watching[keyPath] = 0; - chainsFor(obj, m).remove(keyPath); + chainsFor(obj).remove(keyPath); } else if (watching[keyPath] > 1) { watching[keyPath]--; } @@ -4131,14 +4128,14 @@ function isKeyName(path) { @param obj @param {String} keyName */ -Ember.watch = function(obj, _keyPath, m) { +Ember.watch = function(obj, _keyPath) { // can't watch length on Array - it is special... if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } if (isKeyName(_keyPath)) { - watchKey(obj, _keyPath, m); + watchKey(obj, _keyPath); } else { - watchPath(obj, _keyPath, m); + watchPath(obj, _keyPath); } }; @@ -4149,14 +4146,14 @@ Ember.isWatching = function isWatching(obj, key) { Ember.watch.flushPending = Ember.flushPendingChains; -Ember.unwatch = function(obj, _keyPath, m) { +Ember.unwatch = function(obj, _keyPath) { // can't watch length on Array - it is special... if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } if (isKeyName(_keyPath)) { - unwatchKey(obj, _keyPath, m); + unwatchKey(obj, _keyPath); } else { - unwatchPath(obj, _keyPath, m); + unwatchPath(obj, _keyPath); } }; @@ -4171,7 +4168,7 @@ Ember.unwatch = function(obj, _keyPath, m) { @param obj */ Ember.rewatch = function(obj) { - var m = obj[META_KEY], chains = m && m.chains; + var m = metaFor(obj, false), chains = m.chains; // make sure the object has its own guid. if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { @@ -4297,7 +4294,7 @@ function addDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) + 1; // Watch the depKey - watch(obj, depKey, meta); + watch(obj, depKey); } } @@ -4316,7 +4313,7 @@ function removeDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) - 1; // Watch the depKey - unwatch(obj, depKey, meta); + unwatch(obj, depKey); } } @@ -4759,8 +4756,7 @@ Ember.computed = function(func) { @return {Object} the cached value */ Ember.cacheFor = function cacheFor(obj, key) { - var meta = obj[META_KEY], - cache = meta && meta.cache; + var cache = metaFor(obj, false).cache; if (cache && key in cache) { return cache[key]; @@ -7134,13 +7130,11 @@ var Mixin, REQUIRED, Alias, a_slice = [].slice, o_create = Ember.create, defineProperty = Ember.defineProperty, - guidFor = Ember.guidFor, - metaFor = Ember.meta, - META_KEY = Ember.META_KEY; + guidFor = Ember.guidFor; function mixinsMeta(obj) { - var m = metaFor(obj, true), ret = m.mixins; + var m = Ember.meta(obj, true), ret = m.mixins; if (!ret) { ret = m.mixins = {}; } else if (!m.hasOwnProperty('mixins')) { @@ -7325,7 +7319,7 @@ function mergeMixins(mixins, m, descs, values, base, keys) { if (props === CONTINUE) { continue; } if (props) { - meta = metaFor(base); + meta = Ember.meta(base); if (base.willMergeMixin) { base.willMergeMixin(props); } concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); mergings = concatenatedMixinProperties('mergedProperties', props, values, base); @@ -7383,7 +7377,7 @@ function connectBindings(obj, m) { } function finishPartial(obj, m) { - connectBindings(obj, m || metaFor(obj)); + connectBindings(obj, m || Ember.meta(obj)); return obj; } @@ -7430,7 +7424,7 @@ function replaceObserversAndListeners(obj, key, observerOrListener) { } function applyMixin(obj, mixins, partial) { - var descs = {}, values = {}, m = metaFor(obj), + var descs = {}, values = {}, m = Ember.meta(obj), key, value, desc, keys = []; // Go through all mixins and hashes passed in, and: @@ -7642,8 +7636,7 @@ function _detect(curMixin, targetMixin, seen) { MixinPrototype.detect = function(obj) { if (!obj) { return false; } if (obj instanceof Mixin) { return _detect(obj, this, {}); } - var m = obj[META_KEY], - mixins = m && m.mixins; + var mixins = Ember.meta(obj, false).mixins; if (mixins) { return !!mixins[guidFor(this)]; } @@ -7682,8 +7675,7 @@ MixinPrototype.keys = function() { // returns the mixins currently applied to the specified object // TODO: Make Ember.mixin Mixin.mixins = function(obj) { - var m = obj[META_KEY], - mixins = m && m.mixins, ret = []; + var mixins = Ember.meta(obj, false).mixins, ret = []; if (!mixins) { return ret; } @@ -9579,7 +9571,7 @@ define("rsvp/promise/all", ``` @method all - @for Ember.RSVP.Promise + @for RSVP.Promise @param {Array} entries array of promises @param {String} label optional string for labeling the promise. Useful for tooling. @@ -12215,7 +12207,6 @@ var set = Ember.set, get = Ember.get, guidFor = Ember.guidFor, generateGuid = Ember.generateGuid, meta = Ember.meta, - META_KEY = Ember.META_KEY, rewatch = Ember.rewatch, finishChains = Ember.finishChains, sendEvent = Ember.sendEvent, @@ -12909,8 +12900,7 @@ var ClassMixin = Mixin.create({ @param key {String} property name */ metaForProperty: function(key) { - var meta = this.proto()[META_KEY], - desc = meta && meta.descs[key]; + var desc = meta(this.proto(), false).descs[key]; Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); return desc._meta || {}; @@ -25403,8 +25393,9 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { set(this, 'controller', this); }, - defaultLayout: function(context, options){ - Ember.Handlebars.helpers['yield'].call(context, options); + defaultLayout: function(options){ + options.data = {view: options._context}; + Ember.Handlebars.helpers['yield'].apply(this, [options]); }, // during render, isolate keywords @@ -26510,6 +26501,33 @@ var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { return value; }; +/** + This method uses `Ember.Handlebars.get` to lookup a value, then ensures + that the value is escaped properly. + + If `unescaped` is a truthy value then the escaping will not be performed. + + @method getEscaped + @for Ember.Handlebars + @param {Object} root The object to look up the property on + @param {String} path The path to be lookedup + @param {Object} options The template's option hash +*/ +Ember.Handlebars.getEscaped = function(root, path, options) { + var result = handlebarsGet(root, path, options); + + if (result === null || result === undefined) { + result = ""; + } else if (!(result instanceof Handlebars.SafeString)) { + result = String(result); + } + if (!options.hash.unescaped){ + result = Handlebars.Utils.escapeExpression(result); + } + + return result; +}; + Ember.Handlebars.resolveParams = function(context, params, options) { var resolvedParams = [], types = options.types, param, type; @@ -27467,6 +27485,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; +var handlebarsGetEscaped = Ember.Handlebars.getEscaped; var forEach = Ember.ArrayPolyfills.forEach; var o_create = Ember.create; @@ -27476,20 +27495,6 @@ function exists(value) { return !Ember.isNone(value); } -function sanitizedHandlebarsGet(currentContext, property, options) { - var result = handlebarsGet(currentContext, property, options); - if (result === null || result === undefined) { - result = ""; - } else if (!(result instanceof Handlebars.SafeString)) { - result = String(result); - } - if (!options.hash.unescaped){ - result = Handlebars.Utils.escapeExpression(result); - } - - return result; -} - // Binds a property into the DOM. This will create a hook in DOM that the // KVO system will look for and update if the property changes. function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) { @@ -27560,7 +27565,7 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer } else { // The object is not observable, so just render it out and // be done with it. - data.buffer.push(handlebarsGet(currentContext, property, options)); + data.buffer.push(handlebarsGetEscaped(currentContext, property, options)); } } @@ -27581,7 +27586,7 @@ function simpleBind(currentContext, property, options) { Ember.run.once(view, 'rerender'); }; - output = sanitizedHandlebarsGet(currentContext, property, options); + output = handlebarsGetEscaped(currentContext, property, options); data.buffer.push(output); } else { @@ -27607,8 +27612,7 @@ function simpleBind(currentContext, property, options) { } else { // The object is not observable, so just render it out and // be done with it. - output = sanitizedHandlebarsGet(currentContext, property, options); - + output = handlebarsGetEscaped(currentContext, property, options); data.buffer.push(output); } } @@ -36178,7 +36182,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { if (linkType === 'ID') { options.linkTextPath = linkTitle; options.fn = function() { - return Ember.Handlebars.get(context, linkTitle, options); + return Ember.Handlebars.getEscaped(context, linkTitle, options); }; } else { options.fn = function() { diff --git a/vendor/assets/javascripts/production/ember.js b/vendor/assets/javascripts/production/ember.js index 1539b089cf5..b90d83d297d 100644 --- a/vendor/assets/javascripts/production/ember.js +++ b/vendor/assets/javascripts/production/ember.js @@ -5,7 +5,7 @@ * Portions Copyright 2008-2011 Apple Inc. All rights reserved. * @license Licensed under MIT license * See https://raw.github.com/emberjs/ember.js/master/LICENSE - * @version 1.3.2+pre.773be0ec + * @version 1.3.2 */ @@ -88,7 +88,7 @@ var define, requireModule, require, requirejs; @class Ember @static - @version 1.3.2+pre.773be0ec + @version 1.3.2 */ if ('undefined' === typeof Ember) { @@ -115,10 +115,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.3.2+pre.773be0ec' + @default '1.3.2' @static */ -Ember.VERSION = '1.3.2+pre.773be0ec'; +Ember.VERSION = '1.3.2'; /** Standard environmental variables. You can define these in a global `EmberENV` @@ -831,7 +831,7 @@ Ember.guidFor = function guidFor(obj) { // META // -var META_DESC = Ember.META_DESC = { +var META_DESC = { writable: true, configurable: false, enumerable: false, @@ -2302,7 +2302,7 @@ ObserverSet.prototype.clear = function() { (function() { -var META_KEY = Ember.META_KEY, +var metaFor = Ember.meta, guidFor = Ember.guidFor, tryFinally = Ember.tryFinally, sendEvent = Ember.sendEvent, @@ -2333,10 +2333,10 @@ var META_KEY = Ember.META_KEY, @return {void} */ function propertyWillChange(obj, keyName) { - var m = obj[META_KEY], - watching = (m && m.watching[keyName] > 0) || keyName === 'length', - proto = m && m.proto, - desc = m && m.descs[keyName]; + var m = metaFor(obj, false), + watching = m.watching[keyName] > 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; if (!watching) { return; } if (proto === obj) { return; } @@ -2363,10 +2363,10 @@ Ember.propertyWillChange = propertyWillChange; @return {void} */ function propertyDidChange(obj, keyName) { - var m = obj[META_KEY], - watching = (m && m.watching[keyName] > 0) || keyName === 'length', - proto = m && m.proto, - desc = m && m.descs[keyName]; + var m = metaFor(obj, false), + watching = m.watching[keyName] > 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; if (proto === obj) { return; } @@ -2439,7 +2439,7 @@ function chainsWillChange(obj, keyName, m) { } function chainsDidChange(obj, keyName, m, suppressEvents) { - if (!(m && m.hasOwnProperty('chainWatchers') && + if (!(m.hasOwnProperty('chainWatchers') && m.chainWatchers[keyName])) { return; } @@ -3437,11 +3437,11 @@ var metaFor = Ember.meta, // utils.js MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, o_defineProperty = Ember.platform.defineProperty; -Ember.watchKey = function(obj, keyName, meta) { +Ember.watchKey = function(obj, keyName) { // can't watch length on Array - it is special... if (keyName === 'length' && typeOf(obj) === 'array') { return; } - var m = meta || metaFor(obj), watching = m.watching; + var m = metaFor(obj), watching = m.watching; // activate watching first time if (!watching[keyName]) { @@ -3466,8 +3466,8 @@ Ember.watchKey = function(obj, keyName, meta) { }; -Ember.unwatchKey = function(obj, keyName, meta) { - var m = meta || metaFor(obj), watching = m.watching; +Ember.unwatchKey = function(obj, keyName) { + var m = metaFor(obj), watching = m.watching; if (watching[keyName] === 1) { watching[keyName] = 0; @@ -3510,8 +3510,7 @@ var metaFor = Ember.meta, // utils.js warn = Ember.warn, watchKey = Ember.watchKey, unwatchKey = Ember.unwatchKey, - FIRST_KEY = /^([^\.\*]+)/, - META_KEY = Ember.META_KEY; + FIRST_KEY = /^([^\.\*]+)/; function firstKey(path) { return path.match(FIRST_KEY)[0]; @@ -3545,24 +3544,24 @@ function addChainWatcher(obj, keyName, node) { if (!nodes[keyName]) { nodes[keyName] = []; } nodes[keyName].push(node); - watchKey(obj, keyName, m); + watchKey(obj, keyName); } var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) { if (!obj || 'object' !== typeof obj) { return; } // nothing to do - var m = obj[META_KEY]; - if (m && !m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + var m = metaFor(obj, false); + if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - var nodes = m && m.chainWatchers; + var nodes = m.chainWatchers; - if (nodes && nodes[keyName]) { + if (nodes[keyName]) { nodes = nodes[keyName]; for (var i = 0, l = nodes.length; i < l; i++) { if (nodes[i] === node) { nodes.splice(i, 1); } } } - unwatchKey(obj, keyName, m); + unwatchKey(obj, keyName); }; // A ChainNode watches a single key on an object. If you provide a starting @@ -3602,14 +3601,14 @@ var ChainNodePrototype = ChainNode.prototype; function lazyGet(obj, key) { if (!obj) return undefined; - var meta = obj[META_KEY]; + var meta = metaFor(obj, false); // check if object meant only to be a prototype - if (meta && meta.proto === obj) return undefined; + if (meta.proto === obj) return undefined; if (key === "@each") return get(obj, key); // if a CP only return cached value - var desc = meta && meta.descs[key]; + var desc = meta.descs[key]; if (desc && desc._cacheable) { if (key in meta.cache) { return meta.cache[key]; @@ -3821,14 +3820,12 @@ ChainNodePrototype.didChange = function(events) { }; Ember.finishChains = function(obj) { - // We only create meta if we really have to - var m = obj[META_KEY], chains = m && m.chains; + var m = metaFor(obj, false), chains = m.chains; if (chains) { if (chains.value() !== obj) { - metaFor(obj).chains = chains = chains.copy(obj); - } else { - chains.didChange(null); + m.chains = chains = chains.copy(obj); } + chains.didChange(null); } }; @@ -3850,8 +3847,8 @@ var metaFor = Ember.meta, // utils.js // get the chains for the current object. If the current object has // chains inherited from the proto they will be cloned and reconfigured for // the current object. -function chainsFor(obj, meta) { - var m = meta || metaFor(obj), ret = m.chains; +function chainsFor(obj) { + var m = metaFor(obj), ret = m.chains; if (!ret) { ret = m.chains = new ChainNode(null, null, obj); } else if (ret.value() !== obj) { @@ -3860,26 +3857,26 @@ function chainsFor(obj, meta) { return ret; } -Ember.watchPath = function(obj, keyPath, meta) { +Ember.watchPath = function(obj, keyPath) { // can't watch length on Array - it is special... if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - var m = meta || metaFor(obj), watching = m.watching; + var m = metaFor(obj), watching = m.watching; if (!watching[keyPath]) { // activate watching first time watching[keyPath] = 1; - chainsFor(obj, m).add(keyPath); + chainsFor(obj).add(keyPath); } else { watching[keyPath] = (watching[keyPath] || 0) + 1; } }; -Ember.unwatchPath = function(obj, keyPath, meta) { - var m = meta || metaFor(obj), watching = m.watching; +Ember.unwatchPath = function(obj, keyPath) { + var m = metaFor(obj), watching = m.watching; if (watching[keyPath] === 1) { watching[keyPath] = 0; - chainsFor(obj, m).remove(keyPath); + chainsFor(obj).remove(keyPath); } else if (watching[keyPath] > 1) { watching[keyPath]--; } @@ -3923,14 +3920,14 @@ function isKeyName(path) { @param obj @param {String} keyName */ -Ember.watch = function(obj, _keyPath, m) { +Ember.watch = function(obj, _keyPath) { // can't watch length on Array - it is special... if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } if (isKeyName(_keyPath)) { - watchKey(obj, _keyPath, m); + watchKey(obj, _keyPath); } else { - watchPath(obj, _keyPath, m); + watchPath(obj, _keyPath); } }; @@ -3941,14 +3938,14 @@ Ember.isWatching = function isWatching(obj, key) { Ember.watch.flushPending = Ember.flushPendingChains; -Ember.unwatch = function(obj, _keyPath, m) { +Ember.unwatch = function(obj, _keyPath) { // can't watch length on Array - it is special... if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } if (isKeyName(_keyPath)) { - unwatchKey(obj, _keyPath, m); + unwatchKey(obj, _keyPath); } else { - unwatchPath(obj, _keyPath, m); + unwatchPath(obj, _keyPath); } }; @@ -3963,7 +3960,7 @@ Ember.unwatch = function(obj, _keyPath, m) { @param obj */ Ember.rewatch = function(obj) { - var m = obj[META_KEY], chains = m && m.chains; + var m = metaFor(obj, false), chains = m.chains; // make sure the object has its own guid. if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { @@ -4088,7 +4085,7 @@ function addDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) + 1; // Watch the depKey - watch(obj, depKey, meta); + watch(obj, depKey); } } @@ -4107,7 +4104,7 @@ function removeDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) - 1; // Watch the depKey - unwatch(obj, depKey, meta); + unwatch(obj, depKey); } } @@ -4550,8 +4547,7 @@ Ember.computed = function(func) { @return {Object} the cached value */ Ember.cacheFor = function cacheFor(obj, key) { - var meta = obj[META_KEY], - cache = meta && meta.cache; + var cache = metaFor(obj, false).cache; if (cache && key in cache) { return cache[key]; @@ -6922,13 +6918,11 @@ var Mixin, REQUIRED, Alias, a_slice = [].slice, o_create = Ember.create, defineProperty = Ember.defineProperty, - guidFor = Ember.guidFor, - metaFor = Ember.meta, - META_KEY = Ember.META_KEY; + guidFor = Ember.guidFor; function mixinsMeta(obj) { - var m = metaFor(obj, true), ret = m.mixins; + var m = Ember.meta(obj, true), ret = m.mixins; if (!ret) { ret = m.mixins = {}; } else if (!m.hasOwnProperty('mixins')) { @@ -7111,7 +7105,7 @@ function mergeMixins(mixins, m, descs, values, base, keys) { if (props === CONTINUE) { continue; } if (props) { - meta = metaFor(base); + meta = Ember.meta(base); if (base.willMergeMixin) { base.willMergeMixin(props); } concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); mergings = concatenatedMixinProperties('mergedProperties', props, values, base); @@ -7169,7 +7163,7 @@ function connectBindings(obj, m) { } function finishPartial(obj, m) { - connectBindings(obj, m || metaFor(obj)); + connectBindings(obj, m || Ember.meta(obj)); return obj; } @@ -7216,7 +7210,7 @@ function replaceObserversAndListeners(obj, key, observerOrListener) { } function applyMixin(obj, mixins, partial) { - var descs = {}, values = {}, m = metaFor(obj), + var descs = {}, values = {}, m = Ember.meta(obj), key, value, desc, keys = []; // Go through all mixins and hashes passed in, and: @@ -7426,8 +7420,7 @@ function _detect(curMixin, targetMixin, seen) { MixinPrototype.detect = function(obj) { if (!obj) { return false; } if (obj instanceof Mixin) { return _detect(obj, this, {}); } - var m = obj[META_KEY], - mixins = m && m.mixins; + var mixins = Ember.meta(obj, false).mixins; if (mixins) { return !!mixins[guidFor(this)]; } @@ -7466,8 +7459,7 @@ MixinPrototype.keys = function() { // returns the mixins currently applied to the specified object // TODO: Make Ember.mixin Mixin.mixins = function(obj) { - var m = obj[META_KEY], - mixins = m && m.mixins, ret = []; + var mixins = Ember.meta(obj, false).mixins, ret = []; if (!mixins) { return ret; } @@ -9361,7 +9353,7 @@ define("rsvp/promise/all", ``` @method all - @for Ember.RSVP.Promise + @for RSVP.Promise @param {Array} entries array of promises @param {String} label optional string for labeling the promise. Useful for tooling. @@ -11994,7 +11986,6 @@ var set = Ember.set, get = Ember.get, guidFor = Ember.guidFor, generateGuid = Ember.generateGuid, meta = Ember.meta, - META_KEY = Ember.META_KEY, rewatch = Ember.rewatch, finishChains = Ember.finishChains, sendEvent = Ember.sendEvent, @@ -12682,8 +12673,7 @@ var ClassMixin = Mixin.create({ @param key {String} property name */ metaForProperty: function(key) { - var meta = this.proto()[META_KEY], - desc = meta && meta.descs[key]; + var desc = meta(this.proto(), false).descs[key]; return desc._meta || {}; }, @@ -25122,8 +25112,9 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { set(this, 'controller', this); }, - defaultLayout: function(context, options){ - Ember.Handlebars.helpers['yield'].call(context, options); + defaultLayout: function(options){ + options.data = {view: options._context}; + Ember.Handlebars.helpers['yield'].apply(this, [options]); }, // during render, isolate keywords @@ -26212,6 +26203,33 @@ var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { return value; }; +/** + This method uses `Ember.Handlebars.get` to lookup a value, then ensures + that the value is escaped properly. + + If `unescaped` is a truthy value then the escaping will not be performed. + + @method getEscaped + @for Ember.Handlebars + @param {Object} root The object to look up the property on + @param {String} path The path to be lookedup + @param {Object} options The template's option hash +*/ +Ember.Handlebars.getEscaped = function(root, path, options) { + var result = handlebarsGet(root, path, options); + + if (result === null || result === undefined) { + result = ""; + } else if (!(result instanceof Handlebars.SafeString)) { + result = String(result); + } + if (!options.hash.unescaped){ + result = Handlebars.Utils.escapeExpression(result); + } + + return result; +}; + Ember.Handlebars.resolveParams = function(context, params, options) { var resolvedParams = [], types = options.types, param, type; @@ -27162,6 +27180,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; +var handlebarsGetEscaped = Ember.Handlebars.getEscaped; var forEach = Ember.ArrayPolyfills.forEach; var o_create = Ember.create; @@ -27171,20 +27190,6 @@ function exists(value) { return !Ember.isNone(value); } -function sanitizedHandlebarsGet(currentContext, property, options) { - var result = handlebarsGet(currentContext, property, options); - if (result === null || result === undefined) { - result = ""; - } else if (!(result instanceof Handlebars.SafeString)) { - result = String(result); - } - if (!options.hash.unescaped){ - result = Handlebars.Utils.escapeExpression(result); - } - - return result; -} - // Binds a property into the DOM. This will create a hook in DOM that the // KVO system will look for and update if the property changes. function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) { @@ -27255,7 +27260,7 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer } else { // The object is not observable, so just render it out and // be done with it. - data.buffer.push(handlebarsGet(currentContext, property, options)); + data.buffer.push(handlebarsGetEscaped(currentContext, property, options)); } } @@ -27276,7 +27281,7 @@ function simpleBind(currentContext, property, options) { Ember.run.once(view, 'rerender'); }; - output = sanitizedHandlebarsGet(currentContext, property, options); + output = handlebarsGetEscaped(currentContext, property, options); data.buffer.push(output); } else { @@ -27302,8 +27307,7 @@ function simpleBind(currentContext, property, options) { } else { // The object is not observable, so just render it out and // be done with it. - output = sanitizedHandlebarsGet(currentContext, property, options); - + output = handlebarsGetEscaped(currentContext, property, options); data.buffer.push(output); } } @@ -35799,7 +35803,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { if (linkType === 'ID') { options.linkTextPath = linkTitle; options.fn = function() { - return Ember.Handlebars.get(context, linkTitle, options); + return Ember.Handlebars.getEscaped(context, linkTitle, options); }; } else { options.fn = function() { From de9e1a50492d532e26af0708d7d1ad22a1698d26 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 10 Feb 2014 09:21:46 +1100 Subject: [PATCH 06/27] BUGFIX: allow sorting prior to sidekiq starting --- lib/scheduler/web.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/scheduler/web.rb b/lib/scheduler/web.rb index 72547e6bc16..e3c6c84cd18 100644 --- a/lib/scheduler/web.rb +++ b/lib/scheduler/web.rb @@ -8,7 +8,15 @@ module Scheduler RailsMultisite::ConnectionManagement.with_connection("default") do @manager = Scheduler::Manager.without_runner @schedules = Scheduler::Manager.discover_schedules.sort do |a,b| - a.schedule_info.next_run <=> b.schedule_info.next_run + a_next = a.schedule_info.next_run + b_next = b.schedule_info.next_run + if a_next && b_next + a_next <=> b_next + elsif a_next + -1 + else + 1 + end end erb File.read(File.join(VIEWS, 'scheduler.erb')), locals: {view_path: VIEWS} end From ca170e46361a7086fa98d51e52475dc440db9029 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 10 Feb 2014 15:21:37 +1100 Subject: [PATCH 07/27] Add memory profiler gem for ruby 2.1 --- Gemfile | 1 + Gemfile_rails4.lock | 2 ++ config/initializers/06-mini_profiler.rb | 1 + 3 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 698477e358c..018b5b37005 100644 --- a/Gemfile +++ b/Gemfile @@ -196,6 +196,7 @@ gem 'lru_redux' gem 'flamegraph', require: false gem 'rack-mini-profiler', require: false +gem 'memory_profiler', require: false, platform: :mri_21 # used for caching, optional gem 'rack-cors', require: false diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock index 5989d7838e3..cd1fafcb493 100644 --- a/Gemfile_rails4.lock +++ b/Gemfile_rails4.lock @@ -163,6 +163,7 @@ GEM mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) + memory_profiler (0.0.4) message_bus (0.9.4) eventmachine rack (>= 1.1.3) @@ -425,6 +426,7 @@ DEPENDENCIES librarian (>= 0.0.25) listen (= 0.7.3) lru_redux + memory_profiler message_bus minitest mocha diff --git a/config/initializers/06-mini_profiler.rb b/config/initializers/06-mini_profiler.rb index 8c5f385f7c5..34d6193ce39 100644 --- a/config/initializers/06-mini_profiler.rb +++ b/config/initializers/06-mini_profiler.rb @@ -2,6 +2,7 @@ if Rails.configuration.respond_to?(:enable_mini_profiler) && Rails.configuration.enable_mini_profiler require 'rack-mini-profiler' require 'flamegraph' + require 'memory_profiler' if RUBY_VERSION >= "2.1.0" # initialization is skipped so trigger it Rack::MiniProfilerRails.initialize!(Rails.application) end From 5d0cae2dd1c11672e31c209ff9161546aa2be444 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 10 Feb 2014 15:26:09 +1100 Subject: [PATCH 08/27] BUGFIX: seen logic was incomplete, you can have user_data and still unseen topic --- app/serializers/listable_topic_serializer.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/serializers/listable_topic_serializer.rb b/app/serializers/listable_topic_serializer.rb index 87356b7e0a0..703c18af5ea 100644 --- a/app/serializers/listable_topic_serializer.rb +++ b/app/serializers/listable_topic_serializer.rb @@ -29,18 +29,18 @@ class ListableTopicSerializer < BasicTopicSerializer end def seen - object.user_data.present? + return true if !scope || !scope.user + return true if object.user_data && !object.user_data.last_read_post_number.nil? + return true if object.created_at < scope.user.treat_as_new_topic_start_date + false end def unseen - return false if scope.blank? - return false if scope.user.blank? - return false if object.user_data.present? - return false if object.created_at < scope.user.treat_as_new_topic_start_date - true + !seen end def last_read_post_number + return nil unless object.user_data object.user_data.last_read_post_number end alias :include_last_read_post_number? :seen From d3b8ec36d03bb281a9f08499e1f8d1c2d46f077b Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 10 Feb 2014 15:34:08 +1100 Subject: [PATCH 09/27] warning if running an old version of bundler --- Gemfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 018b5b37005..db5f13de315 100644 --- a/Gemfile +++ b/Gemfile @@ -196,7 +196,11 @@ gem 'lru_redux' gem 'flamegraph', require: false gem 'rack-mini-profiler', require: false -gem 'memory_profiler', require: false, platform: :mri_21 +begin + gem 'memory_profiler', require: false, platform: :mri_21 +rescue + puts "WARNING: you are running an old version of bundler, please run 'gem update bundler'" +end # used for caching, optional gem 'rack-cors', require: false From 212ece3e80b096ca580dd549aa9b486fdcd82ab5 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 10 Feb 2014 15:40:32 +1100 Subject: [PATCH 10/27] Remove memory profiler at least until I push a new docker image --- Gemfile | 5 ----- config/initializers/06-mini_profiler.rb | 1 - 2 files changed, 6 deletions(-) diff --git a/Gemfile b/Gemfile index db5f13de315..698477e358c 100644 --- a/Gemfile +++ b/Gemfile @@ -196,11 +196,6 @@ gem 'lru_redux' gem 'flamegraph', require: false gem 'rack-mini-profiler', require: false -begin - gem 'memory_profiler', require: false, platform: :mri_21 -rescue - puts "WARNING: you are running an old version of bundler, please run 'gem update bundler'" -end # used for caching, optional gem 'rack-cors', require: false diff --git a/config/initializers/06-mini_profiler.rb b/config/initializers/06-mini_profiler.rb index 34d6193ce39..8c5f385f7c5 100644 --- a/config/initializers/06-mini_profiler.rb +++ b/config/initializers/06-mini_profiler.rb @@ -2,7 +2,6 @@ if Rails.configuration.respond_to?(:enable_mini_profiler) && Rails.configuration.enable_mini_profiler require 'rack-mini-profiler' require 'flamegraph' - require 'memory_profiler' if RUBY_VERSION >= "2.1.0" # initialization is skipped so trigger it Rack::MiniProfilerRails.initialize!(Rails.application) end From 172d0ffd088d8bd7a40db89c214ff71edcb28e5e Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 10 Feb 2014 15:43:53 +1100 Subject: [PATCH 11/27] oops clean gemfile lock --- Gemfile_rails4.lock | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock index cd1fafcb493..5989d7838e3 100644 --- a/Gemfile_rails4.lock +++ b/Gemfile_rails4.lock @@ -163,7 +163,6 @@ GEM mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) - memory_profiler (0.0.4) message_bus (0.9.4) eventmachine rack (>= 1.1.3) @@ -426,7 +425,6 @@ DEPENDENCIES librarian (>= 0.0.25) listen (= 0.7.3) lru_redux - memory_profiler message_bus minitest mocha From 4f6cef01e48a5f65b76f30a102b6f204b7782bfd Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 10 Feb 2014 16:07:09 +1100 Subject: [PATCH 12/27] fix build --- app/serializers/listable_topic_serializer.rb | 11 ++++++++--- app/serializers/topic_list_item_serializer.rb | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/serializers/listable_topic_serializer.rb b/app/serializers/listable_topic_serializer.rb index 703c18af5ea..56e1ac8c373 100644 --- a/app/serializers/listable_topic_serializer.rb +++ b/app/serializers/listable_topic_serializer.rb @@ -43,17 +43,22 @@ class ListableTopicSerializer < BasicTopicSerializer return nil unless object.user_data object.user_data.last_read_post_number end - alias :include_last_read_post_number? :seen + + def has_user_data + !!object.user_data + end + + alias :include_last_read_post_number? :has_user_data def unread unread_helper.unread_posts end - alias :include_unread? :seen + alias :include_unread? :has_user_data def new_posts unread_helper.new_posts end - alias :include_new_posts? :seen + alias :include_new_posts? :has_user_data def include_excerpt? pinned diff --git a/app/serializers/topic_list_item_serializer.rb b/app/serializers/topic_list_item_serializer.rb index 082029bf56e..96e00be81a6 100644 --- a/app/serializers/topic_list_item_serializer.rb +++ b/app/serializers/topic_list_item_serializer.rb @@ -13,7 +13,7 @@ class TopicListItemSerializer < ListableTopicSerializer def starred object.user_data.starred? end - alias :include_starred? :seen + alias :include_starred? :has_user_data def posters object.posters || [] From 25070dcde78def7e00315c93be11f8270d76ecda Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 10 Feb 2014 11:15:42 -0500 Subject: [PATCH 13/27] FIX: Show the renamed uncategorized name instead of "(no category)" --- .../discourse/views/category_chooser_view.js | 2 +- .../discourse/views/combobox_view.js | 46 ++++++++++--------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/discourse/views/category_chooser_view.js b/app/assets/javascripts/discourse/views/category_chooser_view.js index 9615f85f150..cb6712c8bcb 100644 --- a/app/assets/javascripts/discourse/views/category_chooser_view.js +++ b/app/assets/javascripts/discourse/views/category_chooser_view.js @@ -26,7 +26,7 @@ Discourse.CategoryChooserView = Discourse.ComboboxView.extend({ none: function() { if (Discourse.User.currentProp('staff') || Discourse.SiteSettings.allow_uncategorized_topics) { - return 'category.none'; + return Discourse.Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id')); } else { return 'category.choose'; } diff --git a/app/assets/javascripts/discourse/views/combobox_view.js b/app/assets/javascripts/discourse/views/combobox_view.js index 07364d8db51..8469c0f707d 100644 --- a/app/assets/javascripts/discourse/views/combobox_view.js +++ b/app/assets/javascripts/discourse/views/combobox_view.js @@ -11,41 +11,45 @@ Discourse.ComboboxView = Discourse.View.extend({ classNames: ['combobox'], valueAttribute: 'id', - render: function(buffer) { + buildData: function(o) { + var data = ""; + if (this.dataAttributes) { + this.dataAttributes.forEach(function(a) { + data += "data-" + a + "=\"" + o.get(a) + "\" "; + }); + } + return data; + }, - var nameProperty = this.get('nameProperty') || 'name'; + render: function(buffer) { + var nameProperty = this.get('nameProperty') || 'name', + none = this.get('none'); // Add none option if required - if (this.get('none')) { - buffer.push('"); + if (typeof none === "string") { + buffer.push('"); + } else if (typeof none === "object") { + buffer.push(""); } var selected = this.get('value'); if (selected) { selected = selected.toString(); } if (this.get('content')) { - - var comboboxView = this; - _.each(this.get('content'),function(o) { - var val = o[comboboxView.get('valueAttribute')]; + var self = this; + this.get('content').forEach(function(o) { + var val = o[self.get('valueAttribute')]; if (val) { val = val.toString(); } var selectedText = (val === selected) ? "selected" : ""; - - var data = ""; - if (comboboxView.dataAttributes) { - comboboxView.dataAttributes.forEach(function(a) { - data += "data-" + a + "=\"" + o.get(a) + "\" "; - }); - } - buffer.push(""); + buffer.push(""); }); } }, valueChanged: function() { - var $combo = this.$(); - var val = this.get('value'); + var $combo = this.$(), + val = this.get('value'); if (val !== undefined && val !== null) { $combo.val(val.toString()); } else { @@ -55,8 +59,8 @@ Discourse.ComboboxView = Discourse.View.extend({ }.observes('value'), didInsertElement: function() { - var $elem = this.$(); - var comboboxView = this; + var $elem = this.$(), + self = this; $elem.chosen({ template: this.template, disable_search_threshold: 5 }); if (this.overrideWidths) { @@ -74,7 +78,7 @@ Discourse.ComboboxView = Discourse.View.extend({ } $elem.chosen().change(function(e) { - comboboxView.set('value', $(e.target).val()); + self.set('value', $(e.target).val()); }); } From b61b33c0fa2fbb5cbe2ae0c2985e5da8709ca530 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 10 Feb 2014 12:30:36 -0500 Subject: [PATCH 14/27] Add groups list to the poster expansion when you click an avatar while in a topic. --- .../components/groups_list_component.js | 12 ++++++++++++ .../javascripts/discourse/mixins/scroll_top.js | 18 ++++++++++++++++++ .../components/groups-list.js.handlebars | 6 ++++++ .../templates/poster_expansion.handlebars | 2 ++ .../templates/user/user.js.handlebars | 9 +-------- .../discourse/views/discovery_topics_view.js | 8 +------- .../discourse/views/group_index_view.js | 11 ++++++++++- .../stylesheets/desktop/poster_expansion.scss | 10 ++++++++++ app/assets/stylesheets/desktop/user.scss | 9 ++++++--- 9 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/groups_list_component.js create mode 100644 app/assets/javascripts/discourse/mixins/scroll_top.js create mode 100644 app/assets/javascripts/discourse/templates/components/groups-list.js.handlebars diff --git a/app/assets/javascripts/discourse/components/groups_list_component.js b/app/assets/javascripts/discourse/components/groups_list_component.js new file mode 100644 index 00000000000..0f78ba24f53 --- /dev/null +++ b/app/assets/javascripts/discourse/components/groups_list_component.js @@ -0,0 +1,12 @@ +/** + Displays a list of groups that a user belongs to. + + @class Discourse.GroupsListComponent + @extends Ember.Component + @namespace Discourse + @module Discourse +**/ +Discourse.GroupsListComponent = Em.Component.extend({ + classNames: ['groups'] +}); + diff --git a/app/assets/javascripts/discourse/mixins/scroll_top.js b/app/assets/javascripts/discourse/mixins/scroll_top.js new file mode 100644 index 00000000000..b9c7c68cce8 --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/scroll_top.js @@ -0,0 +1,18 @@ +/** + This mixin will cause a view to scroll the viewport to the top once it has been inserted + + @class Discourse.ScrollTop + @extends Ember.Mixin + @namespace Discourse + @module Discourse +**/ +Discourse.ScrollTop = Em.Mixin.create({ + + _scrollTop: function() { + Em.run.schedule('afterRender', function() { + $(document).scrollTop(0); + }); + }.on('didInsertElement'), + +}); + diff --git a/app/assets/javascripts/discourse/templates/components/groups-list.js.handlebars b/app/assets/javascripts/discourse/templates/components/groups-list.js.handlebars new file mode 100644 index 00000000000..95a012d1e19 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/groups-list.js.handlebars @@ -0,0 +1,6 @@ +{{#if groups}} + {{i18n groups.title count=groups.length}}: + {{#each groups}} + {{#link-to 'group' this class="group-link"}}{{name}}{{/link-to}} + {{/each}} +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/poster_expansion.handlebars b/app/assets/javascripts/discourse/templates/poster_expansion.handlebars index 0c52cb78ec4..fdb3fdef282 100644 --- a/app/assets/javascripts/discourse/templates/poster_expansion.handlebars +++ b/app/assets/javascripts/discourse/templates/poster_expansion.handlebars @@ -10,6 +10,8 @@

{{i18n last_post}} {{unboundDate path="user.last_posted_at" leaveAgo="true"}}

{{i18n joined}} {{unboundDate path="user.created_at" leaveAgo="true"}}

+ {{groups-list groups=user.custom_groups}} +
{{#if user.bio_cooked}}
{{{user.bio_cooked}}}
{{/if}} diff --git a/app/assets/javascripts/discourse/templates/user/user.js.handlebars b/app/assets/javascripts/discourse/templates/user/user.js.handlebars index beac824acac..259aaa1b57d 100644 --- a/app/assets/javascripts/discourse/templates/user/user.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/user.js.handlebars @@ -43,14 +43,7 @@
{{{bio_cooked}}}
- {{#if custom_groups}} -
- {{i18n groups.title count=custom_groups.length}}: - {{#each custom_groups}} - {{#link-to 'group' this class="group-link"}}{{name}}{{/link-to}} - {{/each}} -
- {{/if}} + {{groups-list groups=custom_groups}} {{#if isSuspended}}
diff --git a/app/assets/javascripts/discourse/views/discovery_topics_view.js b/app/assets/javascripts/discourse/views/discovery_topics_view.js index 0256dbc499d..00b9528bace 100644 --- a/app/assets/javascripts/discourse/views/discovery_topics_view.js +++ b/app/assets/javascripts/discourse/views/discovery_topics_view.js @@ -7,15 +7,9 @@ @namespace Discourse @module Discourse **/ -Discourse.DiscoveryTopicsView = Discourse.View.extend(Discourse.LoadMore, { +Discourse.DiscoveryTopicsView = Discourse.View.extend(Discourse.ScrollTop, Discourse.LoadMore, { eyelineSelector: '.topic-list-item', - _scrollTop: function() { - Em.run.schedule('afterRender', function() { - $(document).scrollTop(0); - }); - }.on('didInsertElement'), - actions: { loadMore: function() { var self = this; diff --git a/app/assets/javascripts/discourse/views/group_index_view.js b/app/assets/javascripts/discourse/views/group_index_view.js index c73a56d4391..71082bb15b8 100644 --- a/app/assets/javascripts/discourse/views/group_index_view.js +++ b/app/assets/javascripts/discourse/views/group_index_view.js @@ -1,4 +1,13 @@ -Discourse.GroupIndexView = Discourse.View.extend(Discourse.LoadMore, { +/** + Displays all posts within a group + + @class Discourse.GroupIndexView + @extends Ember.Mixin + @namespace Discourse + @module Discourse +**/ +Discourse.GroupIndexView = Discourse.View.extend(Discourse.ScrollTop, Discourse.LoadMore, { eyelineSelector: '.user-stream .item' + }); diff --git a/app/assets/stylesheets/desktop/poster_expansion.scss b/app/assets/stylesheets/desktop/poster_expansion.scss index f32773f47b0..c5e60915320 100644 --- a/app/assets/stylesheets/desktop/poster_expansion.scss +++ b/app/assets/stylesheets/desktop/poster_expansion.scss @@ -40,6 +40,16 @@ margin-top: 0px; color: $primary_medium; } + .groups { + font-size: 13px; + font-weight: normal; + margin-top: 0px; + color: $primary_medium; + + .group-link { + color: $primary; + } + } .bottom { clear: both; diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 3c2f93364e7..3e76c2cd4ed 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -2,6 +2,12 @@ @import "common/foundation/variables"; @import "common/foundation/mixins"; +.groups { + .group-link { + color: $tertiary_lightest; + } +} + .user-preferences { input.category-group { width: 500px; @@ -210,9 +216,6 @@ h1, h2 {margin-top: 10px;} - .group-link { - color: $tertiary_lightest; - } .bio { color: $primary_lighter; From 45afed87201ca2d270ab74137310f9d394664768 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 10 Feb 2014 12:43:17 -0500 Subject: [PATCH 15/27] FIX: Chrome has a bug where if you request the same URL with a different MIME type, then leave and hit the back button, you'll get the last MIME type requested instead of HTML. This fixes it. --- app/assets/javascripts/admin/models/admin_dashboard.js | 4 ++-- app/assets/javascripts/admin/models/admin_user.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/models/admin_dashboard.js b/app/assets/javascripts/admin/models/admin_dashboard.js index 14b03e8494d..4aaf0984687 100644 --- a/app/assets/javascripts/admin/models/admin_dashboard.js +++ b/app/assets/javascripts/admin/models/admin_dashboard.js @@ -19,7 +19,7 @@ Discourse.AdminDashboard.reopenClass({ @return {jqXHR} a jQuery Promise object **/ find: function() { - return Discourse.ajax("/admin/dashboard").then(function(json) { + return Discourse.ajax("/admin/dashboard.json").then(function(json) { var model = Discourse.AdminDashboard.create(json); model.set('loaded', true); return model; @@ -34,7 +34,7 @@ Discourse.AdminDashboard.reopenClass({ @return {jqXHR} a jQuery Promise object **/ fetchProblems: function() { - return Discourse.ajax("/admin/dashboard/problems", { + return Discourse.ajax("/admin/dashboard/problems.json", { type: 'GET', dataType: 'json' }).then(function(json) { diff --git a/app/assets/javascripts/admin/models/admin_user.js b/app/assets/javascripts/admin/models/admin_user.js index a55873a619e..068eec8aa18 100644 --- a/app/assets/javascripts/admin/models/admin_user.js +++ b/app/assets/javascripts/admin/models/admin_user.js @@ -388,7 +388,7 @@ Discourse.AdminUser.reopenClass({ }, find: function(username) { - return Discourse.ajax("/admin/users/" + username).then(function (result) { + return Discourse.ajax("/admin/users/" + username + ".json").then(function (result) { result.loadedDetails = true; return Discourse.AdminUser.create(result); }); From f1a9e52d7e2c1c509027ee38c13eaf68d4a5105a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 10 Feb 2014 14:08:16 -0500 Subject: [PATCH 16/27] When creating/editing a category the parent shoudl be none. --- .../discourse/templates/modal/edit_category.js.handlebars | 2 +- .../javascripts/discourse/views/category_chooser_view.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/modal/edit_category.js.handlebars b/app/assets/javascripts/discourse/templates/modal/edit_category.js.handlebars index df18f0de9f8..5fe53e82517 100644 --- a/app/assets/javascripts/discourse/templates/modal/edit_category.js.handlebars +++ b/app/assets/javascripts/discourse/templates/modal/edit_category.js.handlebars @@ -29,7 +29,7 @@ {{/each}} {{else}} - {{categoryChooser valueAttribute="id" value=parent_category_id categories=parentCategories}} + {{categoryChooser valueAttribute="id" value=parent_category_id categories=parentCategories rootNone=true}} {{/if}} diff --git a/app/assets/javascripts/discourse/views/category_chooser_view.js b/app/assets/javascripts/discourse/views/category_chooser_view.js index cb6712c8bcb..41e54a95291 100644 --- a/app/assets/javascripts/discourse/views/category_chooser_view.js +++ b/app/assets/javascripts/discourse/views/category_chooser_view.js @@ -26,7 +26,11 @@ Discourse.CategoryChooserView = Discourse.ComboboxView.extend({ none: function() { if (Discourse.User.currentProp('staff') || Discourse.SiteSettings.allow_uncategorized_topics) { - return Discourse.Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id')); + if (this.get('rootNone')) { + return "category.none"; + } else { + return Discourse.Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id')); + } } else { return 'category.choose'; } From 2a7b609caed999d52f0c15e86c80ff413a524615 Mon Sep 17 00:00:00 2001 From: Wojciech Zawistowski Date: Mon, 10 Feb 2014 20:29:31 +0100 Subject: [PATCH 17/27] Refactors PostCreator. --- lib/post_creator.rb | 135 +++++++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 65 deletions(-) diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 46f5ecce994..59d1aabd9d2 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -70,19 +70,10 @@ class PostCreator @post.save_reply_relationships end - if @spam - GroupMessage.create( Group[:moderators].name, - :spam_post_blocked, - { user: @user, - limit_once_per: 24.hours, - message_params: {domains: @post.linked_hosts.keys.join(', ')} } ) - elsif @post && !@post.errors.present? && !@opts[:skip_validations] - SpamRulesEnforcer.enforce!(@post) - end - + handle_spam track_latest_on_category - enqueue_jobs + @post end @@ -92,9 +83,7 @@ class PostCreator end def self.before_create_tasks(post) - if post.reply_to_post_number.present? - post.reply_to_user_id ||= Post.select(:user_id).where(topic_id: post.topic_id, post_number: post.reply_to_post_number).first.try(:user_id) - end + set_reply_user_id(post) post.word_count = post.raw.scan(/\w+/).size post.post_number ||= Topic.next_post_number(post.topic_id, post.reply_to_post_number.present?) @@ -108,18 +97,33 @@ class PostCreator post.last_version_at ||= Time.now end + def self.set_reply_user_id(post) + return unless post.reply_to_post_number.present? + + post.reply_to_user_id ||= Post.select(:user_id).where(topic_id: post.topic_id, post_number: post.reply_to_post_number).first.try(:user_id) + end protected - def track_latest_on_category - if @post && @post.errors.count == 0 && @topic && @topic.category_id - Category.where(id: @topic.category_id).update_all(latest_post_id: @post.id) - if @post.post_number == 1 - Category.where(id: @topic.category_id).update_all(latest_topic_id: @topic.id) - end + def handle_spam + if @spam + GroupMessage.create( Group[:moderators].name, + :spam_post_blocked, + { user: @user, + limit_once_per: 24.hours, + message_params: {domains: @post.linked_hosts.keys.join(', ')} } ) + elsif @post && !@post.errors.present? && !@opts[:skip_validations] + SpamRulesEnforcer.enforce!(@post) end end + def track_latest_on_category + return unless @post && @post.errors.count == 0 && @topic && @topic.category_id + + Category.where(id: @topic.category_id).update_all(latest_post_id: @post.id) + Category.where(id: @topic.category_id).update_all(latest_topic_id: @topic.id) if @post.post_number == 1 + end + def ensure_in_allowed_users return unless @topic.private_message? @@ -135,24 +139,25 @@ class PostCreator end def after_post_create - if !@topic.private_message? && @post.post_type != Post.types[:moderator_action] - if @post.post_number > 1 - TopicTrackingState.publish_unread(@post) - end - if SiteSetting.enable_mailing_list_mode - Jobs.enqueue_in( - SiteSetting.email_time_window_mins.minutes, - :notify_mailing_list_subscribers, - post_id: @post.id - ) - end + return if @topic.private_message? || @post.post_type == Post.types[:moderator_action] + + if @post.post_number > 1 + TopicTrackingState.publish_unread(@post) + end + + if SiteSetting.enable_mailing_list_mode + Jobs.enqueue_in( + SiteSetting.email_time_window_mins.minutes, + :notify_mailing_list_subscribers, + post_id: @post.id + ) end end def after_topic_create + return unless @new_topic # Don't publish invisible topics return unless @topic.visible? - return if @topic.private_message? || @post.post_type == Post.types[:moderator_action] @topic.posters = @topic.posters_summary @@ -218,13 +223,13 @@ class PostCreator reply_to_post_number: @opts[:reply_to_post_number]) # Attributes we pass through to the post instance if present - [:post_type, :no_bump, :cooking_options, :image_sizes, :acting_user, :invalidate_oneboxes].each do |a| + [:post_type, :no_bump, :cooking_options, :image_sizes, :acting_user, :invalidate_oneboxes, :cook_method].each do |a| post.send("#{a}=", @opts[a]) if @opts[a].present? end - post.cook_method = @opts[:cook_method] if @opts[:cook_method].present? post.extract_quoted_post_numbers post.created_at = Time.zone.parse(@opts[:created_at].to_s) if @opts[:created_at].present? + @post = post end @@ -249,9 +254,9 @@ class PostCreator end def consider_clearing_flags - if @topic.private_message? && @post.post_number > 1 && @topic.user_id != @post.user_id - clear_possible_flags(@topic) - end + return unless @topic.private_message? && @post.post_number > 1 && @topic.user_id != @post.user_id + + clear_possible_flags(@topic) end def update_user_counts @@ -266,16 +271,16 @@ class PostCreator end def publish - if @post.post_number > 1 - MessageBus.publish("/topic/#{@post.topic_id}",{ - id: @post.id, - created_at: @post.created_at, - user: BasicUserSerializer.new(@post.user).as_json(root: false), - post_number: @post.post_number - }, - group_ids: secure_group_ids(@topic) - ) - end + return unless @post.post_number > 1 + + MessageBus.publish("/topic/#{@post.topic_id}",{ + id: @post.id, + created_at: @post.created_at, + user: BasicUserSerializer.new(@post.user).as_json(root: false), + post_number: @post.post_number + }, + group_ids: secure_group_ids(@topic) + ) end def extract_links @@ -283,26 +288,26 @@ class PostCreator end def track_topic - unless @opts[:auto_track] == false - TopicUser.auto_track(@user.id, @topic.id, TopicUser.notification_reasons[:created_post]) - # Update topic user data - TopicUser.change(@post.user.id, - @post.topic.id, - posted: true, - last_read_post_number: @post.post_number, - seen_post_count: @post.post_number) - end + return if @opts[:auto_track] == false + + TopicUser.auto_track(@user.id, @topic.id, TopicUser.notification_reasons[:created_post]) + # Update topic user data + TopicUser.change(@post.user.id, + @post.topic.id, + posted: true, + last_read_post_number: @post.post_number, + seen_post_count: @post.post_number) end def enqueue_jobs - if @post && !@post.errors.present? - # We need to enqueue jobs after the transaction. Otherwise they might begin before the data has - # been comitted. - topic_id = @opts[:topic_id] || @topic.try(:id) - Jobs.enqueue(:feature_topic_users, topic_id: @topic.id) if topic_id.present? - @post.trigger_post_process - after_post_create - after_topic_create if @new_topic - end + return unless @post && !@post.errors.present? + + # We need to enqueue jobs after the transaction. Otherwise they might begin before the data has + # been comitted. + topic_id = @opts[:topic_id] || @topic.try(:id) + Jobs.enqueue(:feature_topic_users, topic_id: @topic.id) if topic_id.present? + @post.trigger_post_process + after_post_create + after_topic_create end end From b61df08d1b78e3c1afe2cf04e7083b4afdb0e201 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 10 Feb 2014 16:59:36 -0500 Subject: [PATCH 18/27] FEATURE: Admin selector to choose a primary group for a user, display it and apply a CSS class to their posts. --- .../controllers/admin_user_controller.js | 18 +++++++++++ .../admin/routes/admin_user_route.js | 10 ++++-- .../admin/templates/user_index.js.handlebars | 21 ++++++++++++- .../admin/views/admin_user_view.js | 10 ++++++ .../components/post_gap_component.js | 5 ++- .../discourse/templates/post.js.handlebars | 1 + .../javascripts/discourse/views/post_view.js | 10 +++++- .../stylesheets/desktop/topic-post.scss | 18 ++++++++--- app/assets/stylesheets/mobile/topic-post.scss | 1 + app/controllers/admin/users_controller.rb | 8 +++++ app/models/group.rb | 1 + .../admin_detailed_user_serializer.rb | 4 ++- app/serializers/post_serializer.rb | 6 ++++ config/locales/client.en.yml | 2 ++ config/routes.rb | 1 + ...210194146_add_primary_group_id_to_users.rb | 5 +++ lib/guardian.rb | 4 +++ lib/topic_view.rb | 16 ++++++++++ .../admin/users_controller_spec.rb | 23 ++++++++++++++ spec/models/user_spec.rb | 31 +++++++++++++++++++ 20 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/admin/views/admin_user_view.js create mode 100644 db/migrate/20140210194146_add_primary_group_id_to_users.rb diff --git a/app/assets/javascripts/admin/controllers/admin_user_controller.js b/app/assets/javascripts/admin/controllers/admin_user_controller.js index c90516fc4fd..c0674fd6d04 100644 --- a/app/assets/javascripts/admin/controllers/admin_user_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_user_controller.js @@ -24,6 +24,8 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({ return Discourse.SiteSettings.must_approve_users; }.property(), + primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'), + actions: { toggleTitleEdit: function() { this.toggleProperty('editingTitle'); @@ -44,6 +46,22 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({ this.get('model').generateApiKey(); }, + savePrimaryGroup: function() { + var self = this; + Discourse.ajax("/admin/users/" + this.get('id') + "/primary_group", { + type: 'PUT', + data: {primary_group_id: this.get('primary_group_id')} + }).then(function () { + self.set('originalPrimaryGroupId', self.get('primary_group_id')); + }).catch(function() { + bootbox.alert(I18n.t('generic_error')); + }); + }, + + resetPrimaryGroup: function() { + this.set('primary_group_id', this.get('originalPrimaryGroupId')); + }, + regenerateApiKey: function() { var self = this; bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { diff --git a/app/assets/javascripts/admin/routes/admin_user_route.js b/app/assets/javascripts/admin/routes/admin_user_route.js index 4a2086c7db4..f3e74174c03 100644 --- a/app/assets/javascripts/admin/routes/admin_user_route.js +++ b/app/assets/javascripts/admin/routes/admin_user_route.js @@ -23,10 +23,16 @@ Discourse.AdminUserRoute = Discourse.Route.extend({ afterModel: function(adminUser) { var controller = this.controllerFor('adminUser'); - adminUser.loadDetails().then(function () { + return adminUser.loadDetails().then(function () { adminUser.setOriginalTrustLevel(); controller.set('model', adminUser); - window.scrollTo(0, 0); + }); + }, + + setupController: function(controller, model) { + controller.setProperties({ + originalPrimaryGroupId: model.get('primary_group_id'), + model: model }); }, diff --git a/app/assets/javascripts/admin/templates/user_index.js.handlebars b/app/assets/javascripts/admin/templates/user_index.js.handlebars index a764d4cf322..37383757ec9 100644 --- a/app/assets/javascripts/admin/templates/user_index.js.handlebars +++ b/app/assets/javascripts/admin/templates/user_index.js.handlebars @@ -46,6 +46,25 @@
+
+
{{i18n admin.groups.primary}}
+
+ {{#if custom_groups}} + {{combobox content=custom_groups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}} + {{else}} + — + {{/if}} +
+
+ {{#if primaryGroupDirty}} +
+ + +
+ {{/if}} +
+
+
{{i18n user.ip_address.title}}
{{ip_address}}
@@ -317,7 +336,7 @@

- diff --git a/app/assets/javascripts/admin/views/admin_user_view.js b/app/assets/javascripts/admin/views/admin_user_view.js new file mode 100644 index 00000000000..0d430af4f9e --- /dev/null +++ b/app/assets/javascripts/admin/views/admin_user_view.js @@ -0,0 +1,10 @@ +/** + The view class for an Admin User + + @class AdminUserView + @extends Discourse.View + @namespace Discourse + @module Discourse +**/ +Discourse.AdminUserView = Discourse.View.extend(Discourse.ScrollTop); + diff --git a/app/assets/javascripts/discourse/components/post_gap_component.js b/app/assets/javascripts/discourse/components/post_gap_component.js index dfffe9a8217..432619d785b 100644 --- a/app/assets/javascripts/discourse/components/post_gap_component.js +++ b/app/assets/javascripts/discourse/components/post_gap_component.js @@ -25,7 +25,10 @@ Discourse.PostGapComponent = Ember.Component.extend({ if (this.get('loading')) { buffer.push(I18n.t('loading')); } else { - buffer.push(I18n.t('post.gap', {count: this.get('gap.length')})); + var gapLength = this.get('gap.length'); + if (gapLength) { + buffer.push(I18n.t('post.gap', {count: gapLength})); + } } }, diff --git a/app/assets/javascripts/discourse/templates/post.js.handlebars b/app/assets/javascripts/discourse/templates/post.js.handlebars index ffc1975a3a0..dd44c335a84 100644 --- a/app/assets/javascripts/discourse/templates/post.js.handlebars +++ b/app/assets/javascripts/discourse/templates/post.js.handlebars @@ -29,6 +29,7 @@ {{/if}} {{#if user_title}}
{{user_title}}
{{/if}} + {{#if primary_group_name}}{{/if}}
{{else}}
diff --git a/app/assets/javascripts/discourse/views/post_view.js b/app/assets/javascripts/discourse/views/post_view.js index 3181db43cd0..dd3d70db3a6 100644 --- a/app/assets/javascripts/discourse/views/post_view.js +++ b/app/assets/javascripts/discourse/views/post_view.js @@ -12,13 +12,21 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, { classNameBindings: ['postTypeClass', 'selected', 'post.hidden:post-hidden', - 'post.deleted'], + 'post.deleted', + 'groupNameClass'], postBinding: 'content', postTypeClass: function() { return this.get('post.post_type') === Discourse.Site.currentProp('post_types.moderator_action') ? 'moderator' : 'regular'; }.property('post.post_type'), + groupNameClass: function() { + var primaryGroupName = this.get('post.primary_group_name'); + if (primaryGroupName) { + return "group-" + primaryGroupName; + } + }.property('post.primary_group_name'), + // If the cooked content changed, add the quote controls cookedChanged: function() { var postView = this; diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 01684918b00..989efdc0d63 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -461,17 +461,26 @@ iframe { width: 45px; height: 45px; } - - .contents { + + .contents { text-align: center; - a { - display: block; + a { + display: block; margin: 0 auto; width: 45px; } + a.user-group { + margin: 4px 0 0 0; + padding: 0px; + color: $primary_light; + font-size: 80%; + width: 100%; + line-height: 13px; + } + h3 a { display: inline; width: auto; @@ -614,6 +623,7 @@ position: relative; } } + .user-title { margin-top: 8px; color: $primary_light; diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index e4ccdfdf970..70dec9c7ccd 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -442,6 +442,7 @@ iframe { float: left; } + .user-title { color: #aaa; padding-top: 2px; diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 27aec1e007d..b7abad65b06 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -17,6 +17,7 @@ class Admin::UsersController < Admin::AdminController :block, :unblock, :trust_level, + :primary_group, :generate_api_key, :revoke_api_key] @@ -94,6 +95,13 @@ class Admin::UsersController < Admin::AdminController render_serialized(@user, AdminUserSerializer) end + def primary_group + guardian.ensure_can_change_primary_group!(@user) + @user.primary_group_id = params[:primary_group_id] + @user.save! + render nothing: true + end + def trust_level guardian.ensure_can_change_trust_level!(@user) logger = StaffActionLogger.new(current_user) diff --git a/app/models/group.rb b/app/models/group.rb index ef2c3060c0f..d2f6cfb0d0e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -220,6 +220,7 @@ class Group < ActiveRecord::Base if @deletions @deletions.each do |gu| gu.destroy + User.update_all 'primary_group_id = NULL', ['id = ? AND primary_group_id = ?', gu.user_id, gu.group_id] end end @deletions = nil diff --git a/app/serializers/admin_detailed_user_serializer.rb b/app/serializers/admin_detailed_user_serializer.rb index 975c92ea6f7..685c34c1c45 100644 --- a/app/serializers/admin_detailed_user_serializer.rb +++ b/app/serializers/admin_detailed_user_serializer.rb @@ -14,12 +14,14 @@ class AdminDetailedUserSerializer < AdminUserSerializer :private_topics_count, :can_delete_all_posts, :can_be_deleted, - :suspend_reason + :suspend_reason, + :primary_group_id has_one :approved_by, serializer: BasicUserSerializer, embed: :objects has_one :api_key, serializer: ApiKeySerializer, embed: :objects has_one :suspended_by, serializer: BasicUserSerializer, embed: :objects has_one :leader_requirements, serializer: LeaderRequirementsSerializer, embed: :objects + has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer def can_revoke_admin scope.can_revoke_admin?(object) diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 5f6253afaf9..9a334d1b66b 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -23,6 +23,7 @@ class PostSerializer < BasicPostSerializer :topic_slug, :topic_id, :display_username, + :primary_group_name, :version, :can_edit, :can_delete, @@ -75,6 +76,11 @@ class PostSerializer < BasicPostSerializer object.user.try(:name) end + def primary_group_name + return nil unless object.user + return @topic_view.primary_group_names[object.user.primary_group_id] if object.user.primary_group_id + end + def link_counts return @single_post_link_counts if @single_post_link_counts.present? diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a2261108a13..3b818c733d8 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1267,6 +1267,8 @@ en: other: "spam x{{count}}" groups: + primary: "Primary Group" + no_primary: "(no primary group)" title: "Groups" edit: "Edit Groups" selector_placeholder: "add users" diff --git a/config/routes.rb b/config/routes.rb index e19f563f4c2..4235cfbb32f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,6 +58,7 @@ Discourse::Application.routes.draw do put "block" put "unblock" put "trust_level" + put "primary_group" get "leader_requirements" end diff --git a/db/migrate/20140210194146_add_primary_group_id_to_users.rb b/db/migrate/20140210194146_add_primary_group_id_to_users.rb new file mode 100644 index 00000000000..84d328ff2c5 --- /dev/null +++ b/db/migrate/20140210194146_add_primary_group_id_to_users.rb @@ -0,0 +1,5 @@ +class AddPrimaryGroupIdToUsers < ActiveRecord::Migration + def change + add_column :users, :primary_group_id, :integer, null: true + end +end diff --git a/lib/guardian.rb b/lib/guardian.rb index 4ebe2804bf1..8867fd7232d 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -133,6 +133,10 @@ class Guardian user && is_staff? end + def can_change_primary_group?(user) + user && is_staff? + end + def can_change_trust_level?(user) user && is_staff? end diff --git a/lib/topic_view.rb b/lib/topic_view.rb index cfccb8c6649..1cbec56ab5b 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -111,6 +111,22 @@ class TopicView filter_posts_paged(opts[:page].to_i) end + def primary_group_names + return @group_names if @group_names + + primary_group_ids = Set.new + @posts.each do |p| + primary_group_ids << p.user.primary_group_id if p.user.try(:primary_group_id) + end + + result = {} + unless primary_group_ids.empty? + Group.where(id: primary_group_ids.to_a).pluck(:id, :name).each do |g| + result[g[0]] = g[1] + end + end + result + end # Find the sort order for a post in the topic def sort_order_for_post_number(post_number) diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 358aefbe2ba..cf9aa4423eb 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -140,6 +140,29 @@ describe Admin::UsersController do end end + context '.primary_group' do + before do + @another_user = Fabricate(:coding_horror) + end + + it "raises an error when the user doesn't have permission" do + Guardian.any_instance.expects(:can_change_primary_group?).with(@another_user).returns(false) + xhr :put, :primary_group, user_id: @another_user.id + response.should be_forbidden + end + + it "returns a 404 if the user doesn't exist" do + xhr :put, :primary_group, user_id: 123123 + response.should be_forbidden + end + + it "chagnes the user's trust level" do + xhr :put, :primary_group, user_id: @another_user.id, primary_group_id: 2 + @another_user.reload + @another_user.primary_group_id.should == 2 + end + end + context '.trust_level' do before do @another_user = Fabricate(:coding_horror) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7d09fdbc0bb..8a6b2e4251e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1050,4 +1050,35 @@ describe User do end end + describe "primary_group_id" do + let!(:user) { Fabricate(:user) } + + it "has no primary_group_id by default" do + user.primary_group_id.should be_nil + end + + context "when the user has a group" do + let!(:group) { Fabricate(:group) } + + before do + group.usernames = user.username + group.save + user.primary_group_id = group.id + user.save + user.reload + end + + it "should allow us to use it as a primary group" do + user.primary_group_id.should == group.id + + # If we remove the user from the group + group.usernames = "" + group.save + + # It should unset it from the primary_group_id + user.reload + user.primary_group_id.should be_nil + end + end + end end From 14449f236a30ef9a306f3d0a77497afdb04618b4 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 10 Feb 2014 17:07:39 -0500 Subject: [PATCH 19/27] FIX: Back button in chrome on static pages --- .../javascripts/discourse/controllers/static_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/static_controller.js b/app/assets/javascripts/discourse/controllers/static_controller.js index 05b6d5c8ce5..2e98cf0f0c0 100644 --- a/app/assets/javascripts/discourse/controllers/static_controller.js +++ b/app/assets/javascripts/discourse/controllers/static_controller.js @@ -29,7 +29,7 @@ Discourse.StaticController = Discourse.Controller.extend({ text = text.match(/((?:.|[\n\r])*)/)[1]; this.set('content', text); } else { - return Discourse.ajax(path, {dataType: 'html'}).then(function (result) { + return Discourse.ajax(path + ".html", {dataType: 'html'}).then(function (result) { self.set('content', result); }); } From ca17f8a437b5ae43f9b15ca67242d6cad0f56f51 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 10 Feb 2014 17:36:13 -0500 Subject: [PATCH 20/27] FIX: Wrong text. Thanks @riking --- spec/controllers/admin/users_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index cf9aa4423eb..b974dbc6390 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -156,7 +156,7 @@ describe Admin::UsersController do response.should be_forbidden end - it "chagnes the user's trust level" do + it "changes the user's primary group" do xhr :put, :primary_group, user_id: @another_user.id, primary_group_id: 2 @another_user.reload @another_user.primary_group_id.should == 2 From 820fe572ca87ace3b6d85dad6934b1e60c7c8a3c Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 11 Feb 2014 10:06:20 +1100 Subject: [PATCH 21/27] BUGFIX: phantom new and unread for category definition topics --- app/controllers/list_controller.rb | 5 +++++ lib/topic_query.rb | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 6913ea6b862..79ad80cc5e2 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -40,6 +40,11 @@ class ListController < ApplicationController list_opts = build_topic_list_options list_opts.merge!(options) if options user = list_target_user + + if filter == :latest && params[:category].blank? + list_opts[:no_definitions] = true + end + list = TopicQuery.new(user, list_opts).public_send("list_#{filter}") list.more_topics_url = construct_url_with(list_opts) if Discourse.anonymous_filters.include?(filter) diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 57dd477cf07..349703b2b42 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -240,8 +240,8 @@ class TopicQuery result = result.listable_topics.includes(category: :topic_only_relative_url) result = result.where('categories.name is null or categories.name <> ?', options[:exclude_category]).references(:categories) if options[:exclude_category] - # Don't include the category topic unless restricted to that category - if options[:category].blank? || options[:no_definitions] + # Don't include the category topics if excluded + if options[:no_definitions] result = result.where('COALESCE(categories.topic_id, 0) <> topics.id') end From 8a4b603bfdbce826c01b937f6ced77f14f911c8d Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 11 Feb 2014 10:06:50 +1100 Subject: [PATCH 22/27] rails master upgrade --- Gemfile_rails_master.lock | 127 +++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 64 deletions(-) diff --git a/Gemfile_rails_master.lock b/Gemfile_rails_master.lock index 950e1a98513..5917d56c1c1 100644 --- a/Gemfile_rails_master.lock +++ b/Gemfile_rails_master.lock @@ -1,3 +1,15 @@ +GIT + remote: https://github.com/dysania/onebox.git + revision: 6a2f6e6a08f183a4df52f9a51187f566b8ae3a00 + specs: + onebox (1.1.0) + hexpress (~> 1.2) + moneta (~> 0.7) + multi_json (~> 1.7) + mustache (~> 0.99) + nokogiri (~> 1.6.1) + opengraph_parser (~> 0.2.3) + GIT remote: https://github.com/rails/actionpack-action_caching.git revision: a45e97298f6a77a4d74662521715d5656b821f24 @@ -7,47 +19,47 @@ GIT GIT remote: https://github.com/rails/rails.git - revision: 4aae538d9ffff3a00a81f3da52fa70f7fd79ac74 + revision: 3a428f38b2f9a1e995070a4a049645b622c7094a specs: - actionmailer (4.1.0.beta) - actionpack (= 4.1.0.beta) - actionview (= 4.1.0.beta) + actionmailer (4.1.0.beta1) + actionpack (= 4.1.0.beta1) + actionview (= 4.1.0.beta1) mail (~> 2.5.4) - actionpack (4.1.0.beta) - actionview (= 4.1.0.beta) - activesupport (= 4.1.0.beta) + actionpack (4.1.0.beta1) + actionview (= 4.1.0.beta1) + activesupport (= 4.1.0.beta1) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.0.beta) - activesupport (= 4.1.0.beta) - builder (~> 3.1.0) + actionview (4.1.0.beta1) + activesupport (= 4.1.0.beta1) + builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.0.beta) - activesupport (= 4.1.0.beta) - builder (~> 3.1.0) - activerecord (4.1.0.beta) - activemodel (= 4.1.0.beta) - activesupport (= 4.1.0.beta) + activemodel (4.1.0.beta1) + activesupport (= 4.1.0.beta1) + builder (~> 3.1) + activerecord (4.1.0.beta1) + activemodel (= 4.1.0.beta1) + activesupport (= 4.1.0.beta1) arel (~> 5.0.0) - activesupport (4.1.0.beta) - i18n (~> 0.6, >= 0.6.4) + activesupport (4.1.0.beta1) + i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) - rails (4.1.0.beta) - actionmailer (= 4.1.0.beta) - actionpack (= 4.1.0.beta) - actionview (= 4.1.0.beta) - activemodel (= 4.1.0.beta) - activerecord (= 4.1.0.beta) - activesupport (= 4.1.0.beta) + rails (4.1.0.beta1) + actionmailer (= 4.1.0.beta1) + actionpack (= 4.1.0.beta1) + actionview (= 4.1.0.beta1) + activemodel (= 4.1.0.beta1) + activerecord (= 4.1.0.beta1) + activesupport (= 4.1.0.beta1) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.0.beta) + railties (= 4.1.0.beta1) sprockets-rails (~> 2.0.0) - railties (4.1.0.beta) - actionpack (= 4.1.0.beta) - activesupport (= 4.1.0.beta) + railties (4.1.0.beta1) + actionpack (= 4.1.0.beta1) + activesupport (= 4.1.0.beta1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) @@ -84,7 +96,7 @@ GEM erubis (>= 2.6.6) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - builder (3.1.4) + builder (3.2.2) celluloid (0.15.2) timers (~> 1.1.0) certified (0.1.1) @@ -144,16 +156,15 @@ GEM fspath (2.0.5) given_core (3.1.1) sorcerer (>= 0.3.7) + guess_html_encoding (0.0.9) handlebars-source (1.1.2) hashie (2.0.5) + hexpress (1.2.0) highline (1.6.20) hike (1.2.3) hiredis (0.4.5) - html_truncator (0.3.1) - nokogiri (~> 1.5) httpauth (0.2.0) i18n (0.6.9) - ice_cube (0.11.1) image_optim (0.9.1) exifr (~> 1.1.3) fspath (~> 2.0.5) @@ -186,19 +197,20 @@ GEM metaclass (0.0.1) method_source (0.8.2) mime-types (1.25.1) - mini_portile (0.5.1) - minitest (5.1.0) + mini_portile (0.5.2) + minitest (5.2.2) mocha (0.14.0) metaclass (~> 0.0.1) mock_redis (0.9.0) + moneta (0.7.20) msgpack (0.5.7) - multi_json (1.8.2) + multi_json (1.8.4) multipart-post (1.2.0) mustache (0.99.4) net-scp (1.1.2) net-ssh (>= 2.6.5) net-ssh (2.7.0) - nokogiri (1.6.0) + nokogiri (1.6.1) mini_portile (~> 0.5.0) oauth (0.4.7) oauth2 (0.8.1) @@ -236,6 +248,9 @@ GEM omniauth-twitter (1.0.1) multi_json (~> 1.3) omniauth-oauth (~> 1.0) + opengraph_parser (0.2.3) + addressable + nokogiri openid-redis-store (0.0.2) redis ruby-openid @@ -269,7 +284,7 @@ GEM rails-observers (0.1.2) activemodel (~> 4.0) raindrops (0.12.0) - rake (10.1.0) + rake (10.1.1) rake-compiler (0.9.2) rake rb-fsevent (0.9.3) @@ -281,24 +296,8 @@ GEM trollop (>= 1.16.2) redcarpet (3.0.0) redis (3.0.6) - redis-actionpack (4.0.0) - actionpack (~> 4) - redis-rack (~> 1.5.0) - redis-store (~> 1.1.0) - redis-activesupport (4.0.0) - activesupport (~> 4) - redis-store (~> 1.1.0) redis-namespace (1.3.2) redis (~> 3.0.4) - redis-rack (1.5.0) - rack (~> 1.5) - redis-store (~> 1.1.0) - redis-rails (4.0.0) - redis-actionpack (~> 4) - redis-activesupport (~> 4) - redis-store (~> 1.1.0) - redis-store (1.1.4) - redis (>= 2.2) ref (1.0.5) rest-client (1.6.7) mime-types (>= 1.16) @@ -323,6 +322,9 @@ GEM rspec-mocks (~> 2.14.0) ruby-hmac (0.4.0) ruby-openid (2.3.0) + ruby-readability (0.6.0) + guess_html_encoding (>= 0.0.4) + nokogiri (>= 1.4.2) sanitize (2.0.6) nokogiri (>= 1.4.4) sass (3.2.12) @@ -347,10 +349,7 @@ GEM redis-namespace (>= 1.3.1) sidekiq-failures (0.2.2) sidekiq (>= 2.9.0) - sidetiq (0.4.3) - celluloid (>= 0.14.1) - ice_cube (~> 0.11.0) - sidekiq (~> 2.15.0) + simple-rss (1.3.1) simplecov (0.7.1) multi_json (~> 1.0) simplecov-html (~> 0.7.1) @@ -378,7 +377,7 @@ GEM activesupport (>= 3.0) sprockets (~> 2.8) temple (0.6.7) - therubyracer-discourse (0.12.0) + therubyracer (0.12.1) libv8 (~> 3.16.14.0) ref thin (1.6.1) @@ -436,7 +435,6 @@ DEPENDENCIES handlebars-source (~> 1.1.2) highline hiredis - html_truncator image_optim image_sorcery librarian (>= 0.0.25) @@ -458,6 +456,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter + onebox! openid-redis-store pg (= 0.15.1) pry-nav @@ -465,7 +464,7 @@ DEPENDENCIES puma qunit-rails rack-cors - rack-mini-profiler (= 0.9.0.pre) + rack-mini-profiler rack-protection rails! rails-observers @@ -476,11 +475,11 @@ DEPENDENCIES rbtrace redcarpet redis - redis-rails rest-client rinku rspec-given rspec-rails + ruby-readability sanitize sass sass-rails @@ -488,12 +487,12 @@ DEPENDENCIES shoulda sidekiq (= 2.15.1) sidekiq-failures - sidetiq (>= 0.3.6) + simple-rss simplecov sinatra slim spork-rails - therubyracer-discourse + therubyracer thin timecop uglifier From 750d3a6b187ca19a3060855f52c59cf82af7d078 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 11 Feb 2014 10:23:47 +1100 Subject: [PATCH 23/27] BUGFIX: ensure topic_view exists before attempting to access it --- app/serializers/post_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 9a334d1b66b..0c168d5edcc 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -77,7 +77,7 @@ class PostSerializer < BasicPostSerializer end def primary_group_name - return nil unless object.user + return nil unless object.user && @topic_view return @topic_view.primary_group_names[object.user.primary_group_id] if object.user.primary_group_id end From 4a35d055bcff0fabdf2aa0075d6f8fc8e9486cbc Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 11 Feb 2014 10:50:14 +1100 Subject: [PATCH 24/27] correct spec to account for category definition topics --- spec/components/topic_query_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index f0bcc754940..c595adcb937 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -30,12 +30,12 @@ describe TopicQuery do # mods can see hidden topics TopicQuery.new(moderator).list_latest.topics.count.should == 1 # admins can see all the topics - TopicQuery.new(admin).list_latest.topics.count.should == 2 + TopicQuery.new(admin).list_latest.topics.count.should == 3 group.add(user) group.save - TopicQuery.new(user).list_latest.topics.count.should == 1 + TopicQuery.new(user).list_latest.topics.count.should == 2 end From 11e962c84853cc4c9502798e688ea9c24ababd76 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 11 Feb 2014 15:28:05 +1100 Subject: [PATCH 25/27] BUGFIX: improve quality of unread / new counters --- .../javascripts/discourse/lib/screen_track.js | 8 ++++++- .../discourse/models/topic_tracking_state.js | 23 ++++++++++++++++--- .../routes/discovery_route_builders.js | 3 +++ .../models/topic_tracking_state_test.js | 21 +++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 test/javascripts/models/topic_tracking_state_test.js diff --git a/app/assets/javascripts/discourse/lib/screen_track.js b/app/assets/javascripts/discourse/lib/screen_track.js index 7d472fb650d..91a6111f7b6 100644 --- a/app/assets/javascripts/discourse/lib/screen_track.js +++ b/app/assets/javascripts/discourse/lib/screen_track.js @@ -37,6 +37,11 @@ Discourse.ScreenTrack = Ember.Object.extend({ }, stop: function() { + if(!this.get('topicId')) { + // already stopped no need to "extra stop" + return; + } + this.tick(); this.flush(); this.reset(); @@ -105,9 +110,10 @@ Discourse.ScreenTrack = Ember.Object.extend({ var highestSeenByTopic = Discourse.Session.currentProp('highestSeenByTopic'); if ((highestSeenByTopic[topicId] || 0) < highestSeen) { highestSeenByTopic[topicId] = highestSeen; - Discourse.TopicTrackingState.current().updateSeen(topicId, highestSeen); } + Discourse.TopicTrackingState.current().updateSeen(topicId, highestSeen); + if (!$.isEmptyObject(newTimings)) { Discourse.ajax('/topics/timings', { data: { diff --git a/app/assets/javascripts/discourse/models/topic_tracking_state.js b/app/assets/javascripts/discourse/models/topic_tracking_state.js index 51700570d28..d6b45d073ab 100644 --- a/app/assets/javascripts/discourse/models/topic_tracking_state.js +++ b/app/assets/javascripts/discourse/models/topic_tracking_state.js @@ -39,8 +39,9 @@ Discourse.TopicTrackingState = Discourse.Model.extend({ }, updateSeen: function(topicId, highestSeen) { + if(!topicId || !highestSeen) { return; } var state = this.states["t" + topicId]; - if(state && state.last_read_post_number < highestSeen) { + if(state && (!state.last_read_post_number || state.last_read_post_number < highestSeen)) { state.last_read_post_number = highestSeen; this.incrementMessageCount(); } @@ -84,9 +85,24 @@ Discourse.TopicTrackingState = Discourse.Model.extend({ sync: function(list, filter){ var tracker = this; + var states = this.states; if(!list || !list.topics) { return; } + // compensate for delayed "new" topics + // client side we know they are not new, server side we think they are + for(var i=list.topics.length-1; i>=0; i--){ + var state = states["t"+ list.topics[i].id]; + if(state && state.last_read_post_number > 0){ + if(filter === "new"){ + list.topics.splice(i, 1); + } else { + list.topics[i].unseen = false; + list.topics[i].dont_sync = true; + } + } + } + if(filter === "new" && !list.more_topics_url){ // scrub all new rows and reload from list _.each(this.states, function(state){ @@ -112,10 +128,11 @@ Discourse.TopicTrackingState = Discourse.Model.extend({ if(topic.unseen) { row.last_read_post_number = null; } else if (topic.unread || topic.new_posts){ - // subtle issue here row.last_read_post_number = topic.highest_post_number - ((topic.unread||0) + (topic.new_posts||0)); } else { - delete tracker.states["t" + topic.id]; + if(!topic.dont_sync) { + delete tracker.states["t" + topic.id]; + } return; } diff --git a/app/assets/javascripts/discourse/routes/discovery_route_builders.js b/app/assets/javascripts/discourse/routes/discovery_route_builders.js index 86487c194cb..53e7ac19f31 100644 --- a/app/assets/javascripts/discourse/routes/discovery_route_builders.js +++ b/app/assets/javascripts/discourse/routes/discovery_route_builders.js @@ -11,6 +11,9 @@ function buildTopicRoute(filter) { }, model: function() { + // attempt to stop early cause we need this to be called before .sync + Discourse.ScreenTrack.current().stop(); + return Discourse.TopicList.list(filter).then(function(list) { var tracking = Discourse.TopicTrackingState.current(); if (tracking) { diff --git a/test/javascripts/models/topic_tracking_state_test.js b/test/javascripts/models/topic_tracking_state_test.js new file mode 100644 index 00000000000..305fc1d1729 --- /dev/null +++ b/test/javascripts/models/topic_tracking_state_test.js @@ -0,0 +1,21 @@ +module("Discourse.TopicTrackingState"); + +test("sync", function () { + + var state = Discourse.TopicTrackingState.create(); + // fake track it + state.states["t111"] = {last_read_post_number: null}; + + state.updateSeen(111, 7); + var list = {topics: [{ + highest_post_number: null, + id: 111, + unread: 10, + new_posts: 10 + }]}; + + state.sync(list, "new"); + + equal(list.topics.length, 0, "expect new topic to be removed as it was seen"); + +}); From 57ee244394a44636c8cd647b24ef971ce75b5cbb Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 11 Feb 2014 16:11:40 +1100 Subject: [PATCH 26/27] Bump up consistency check to run twice a day instead of once --- app/jobs/scheduled/ensure_db_consistency.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/scheduled/ensure_db_consistency.rb b/app/jobs/scheduled/ensure_db_consistency.rb index 9b52179803f..ca3104522a4 100644 --- a/app/jobs/scheduled/ensure_db_consistency.rb +++ b/app/jobs/scheduled/ensure_db_consistency.rb @@ -1,7 +1,7 @@ module Jobs # various consistency checks class EnsureDbConsistency < Jobs::Scheduled - every 1.day + every 12.hours def execute(args) TopicUser.ensure_consistency! From fd34932068fb4cea3e124bcb7c7c700c89013451 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 11 Feb 2014 16:11:51 +1100 Subject: [PATCH 27/27] use discourse redis, not redis --- spec/components/scheduler/manager_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/components/scheduler/manager_spec.rb b/spec/components/scheduler/manager_spec.rb index 2fab9349f85..b1ff8e0ee57 100644 --- a/spec/components/scheduler/manager_spec.rb +++ b/spec/components/scheduler/manager_spec.rb @@ -25,7 +25,7 @@ describe Scheduler::Manager do end end - let(:manager) { Scheduler::Manager.new(Redis.new) } + let(:manager) { Scheduler::Manager.new(DiscourseRedis.new) } before do $redis.del manager.class.queue_key