diff --git a/app/assets/javascripts/discourse/adapters/rest.js.es6 b/app/assets/javascripts/discourse/adapters/rest.js.es6 index 959bf9bee..da499c963 100644 --- a/app/assets/javascripts/discourse/adapters/rest.js.es6 +++ b/app/assets/javascripts/discourse/adapters/rest.js.es6 @@ -1,31 +1,45 @@ const ADMIN_MODELS = ['plugin']; export default Ember.Object.extend({ - pathFor(type, id) { - let path = "/" + Ember.String.underscore(type + 's'); + pathFor(store, type, findArgs) { + let path = "/" + Ember.String.underscore(store.pluralize(type)); if (ADMIN_MODELS.indexOf(type) !== -1) { path = "/admin/" + path; } - if (id) { path += "/" + id; } + + if (findArgs) { + if (typeof findArgs === "object") { + const queryString = Object.keys(findArgs) + .reject(k => !findArgs[k]) + .map(k => k + "=" + encodeURIComponent(findArgs[k])); + + if (queryString.length) { + path += "?" + queryString.join('&'); + } + } else { + // It's serializable as a string if not an object + path += "/" + findArgs; + } + } return path; }, findAll(store, type) { - return Discourse.ajax(this.pathFor(type)); + return Discourse.ajax(this.pathFor(store, type)); }, - find(store, type, id) { - return Discourse.ajax(this.pathFor(type, id)); + find(store, type, findArgs) { + return Discourse.ajax(this.pathFor(store, type, findArgs)); }, update(store, type, id, attrs) { const data = {}; data[Ember.String.underscore(type)] = attrs; - return Discourse.ajax(this.pathFor(type, id), { method: 'PUT', data }); + return Discourse.ajax(this.pathFor(store, type, id), { method: 'PUT', data }); }, destroyRecord(store, type, record) { - return Discourse.ajax(this.pathFor(type, record.get('id')), { method: 'DELETE' }); + return Discourse.ajax(this.pathFor(store, type, record.get('id')), { method: 'DELETE' }); } }); diff --git a/app/assets/javascripts/discourse/components/directory-toggle.js.es6 b/app/assets/javascripts/discourse/components/directory-toggle.js.es6 new file mode 100644 index 000000000..8c4b4761b --- /dev/null +++ b/app/assets/javascripts/discourse/components/directory-toggle.js.es6 @@ -0,0 +1,28 @@ +import StringBuffer from 'discourse/mixins/string-buffer'; +import { iconHTML } from 'discourse/helpers/fa-icon'; + +export default Ember.Component.extend(StringBuffer, { + tagName: 'th', + classNames: ['sortable'], + rerenderTriggers: ['order', 'asc'], + + renderString(buffer) { + const field = this.get('field'); + buffer.push(I18n.t('directory.' + field)); + + if (field === this.get('order')) { + buffer.push(iconHTML(this.get('asc') ? 'chevron-up' : 'chevron-down')); + } + }, + + click() { + const currentOrder = this.get('order'), + field = this.get('field'); + + if (currentOrder === field) { + this.set('asc', this.get('asc') ? null : true); + } else { + this.setProperties({ order: field, asc: null }); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/top-period-chooser.js.es6 b/app/assets/javascripts/discourse/components/period-chooser.js.es6 similarity index 68% rename from app/assets/javascripts/discourse/components/top-period-chooser.js.es6 rename to app/assets/javascripts/discourse/components/period-chooser.js.es6 index ee63a0ae4..1bbf20c03 100644 --- a/app/assets/javascripts/discourse/components/top-period-chooser.js.es6 +++ b/app/assets/javascripts/discourse/components/period-chooser.js.es6 @@ -10,9 +10,9 @@ export default Ember.Component.extend(CleansUp, { }, _clickToClose: function() { - var self = this; + const self = this; $('html').off('mousedown.top-period').on('mousedown.top-period', function(e) { - var $target = $(e.target); + const $target = $(e.target); if (($target.prop('id') === 'topic-entrance') || (self.$().has($target).length !== 0)) { return; } @@ -20,12 +20,23 @@ export default Ember.Component.extend(CleansUp, { }); }, - click: function() { + click(e) { + if ($(e.target).closest('.period-popup').length) { return; } + if (!this.get('showPeriods')) { - var $chevron = this.$('i.fa-caret-down'); + const $chevron = this.$('i.fa-caret-down'); this.$('#period-popup').css($chevron.position()); this.set('showPeriods', true); this._clickToClose(); } + }, + + actions: { + changePeriod(p) { + this.cleanUp(); + this.set('period', p); + this.sendAction('action', p); + } } + }); diff --git a/app/assets/javascripts/discourse/components/top-period-buttons.js.es6 b/app/assets/javascripts/discourse/components/top-period-buttons.js.es6 index eec8000a4..3273805d7 100644 --- a/app/assets/javascripts/discourse/components/top-period-buttons.js.es6 +++ b/app/assets/javascripts/discourse/components/top-period-buttons.js.es6 @@ -1,3 +1,14 @@ export default Ember.Component.extend({ - classNames: ['top-title-buttons'] + classNames: ['top-title-buttons'], + + periods: function() { + const period = this.get('period'); + return this.site.get('periods').filter(p => p !== period); + }.property('period'), + + actions: { + changePeriod(p) { + this.sendAction('action', p); + } + } }); diff --git a/app/assets/javascripts/discourse/components/top-title-button.js.es6 b/app/assets/javascripts/discourse/components/top-title-button.js.es6 deleted file mode 100644 index 8da2fd507..000000000 --- a/app/assets/javascripts/discourse/components/top-title-button.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -import TopTitle from 'discourse/components/top-title'; - -export default TopTitle.extend({ - tagName: 'button', - classNameBindings: [':btn', ':btn-default', 'unless:hidden'], - - click: function() { - var url = this.get('period.showMoreUrl'); - if (url) { - Discourse.URL.routeTo(url); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/top-title.js.es6 b/app/assets/javascripts/discourse/components/top-title.js.es6 deleted file mode 100644 index cfa065429..000000000 --- a/app/assets/javascripts/discourse/components/top-title.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -import StringBuffer from 'discourse/mixins/string-buffer'; - -export default Ember.Component.extend(StringBuffer, { - tagName: 'h2', - rerenderTriggers: ['period.title'], - - renderString: function(buffer) { - buffer.push(" " + this.get('period.title')); - } -}); diff --git a/app/assets/javascripts/discourse/controllers/directory-show.js.es6 b/app/assets/javascripts/discourse/controllers/directory-show.js.es6 new file mode 100644 index 000000000..2ea363076 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/directory-show.js.es6 @@ -0,0 +1,13 @@ +export default Ember.Controller.extend({ + queryParams: ['order', 'asc'], + order: 'likes_received', + asc: null, + + showTimeRead: Ember.computed.equal('period', 'all'), + + actions: { + loadMore() { + this.get('model').loadMore(); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/discovery.js.es6 b/app/assets/javascripts/discourse/controllers/discovery.js.es6 index 45fa7d295..abaaa1e2b 100644 --- a/app/assets/javascripts/discourse/controllers/discovery.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery.js.es6 @@ -1,5 +1,4 @@ import ObjectController from 'discourse/controllers/object'; -import TopPeriod from 'discourse/models/top-period'; export default ObjectController.extend({ needs: ['navigation/category', 'discovery/topics', 'application'], @@ -15,7 +14,7 @@ export default ObjectController.extend({ }.observes("loadedAllItems"), showMoreUrl(period) { - var url = '', category = this.get('category'); + let url = '', category = this.get('category'); if (category) { url = '/c/' + Discourse.Category.slugFor(category) + (this.get('noSubcategories') ? '/none' : '') + '/l'; } @@ -23,15 +22,10 @@ export default ObjectController.extend({ return url; }, - periods: function() { - const self = this, - periods = []; - this.site.get('periods').forEach(function(p) { - periods.pushObject(TopPeriod.create({ id: p, - showMoreUrl: self.showMoreUrl(p), - periods })); - }); - return periods; - }.property('category', 'noSubcategories'), + actions: { + changePeriod(p) { + Discourse.URL.routeTo(this.showMoreUrl(p)); + } + } }); diff --git a/app/assets/javascripts/discourse/helpers/period-title.js.es6 b/app/assets/javascripts/discourse/helpers/period-title.js.es6 new file mode 100644 index 000000000..c85643934 --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/period-title.js.es6 @@ -0,0 +1,11 @@ +import { iconHTML } from 'discourse/helpers/fa-icon'; + +const TITLE_SUBS = { yearly: 'this_year', + monthly: 'this_month', + daily: 'today', + all: 'all' }; + +export default Ember.Handlebars.makeBoundHelper(function (period) { + const title = I18n.t('filters.top.' + (TITLE_SUBS[period] || 'this_week')); + return new Handlebars.SafeString(iconHTML('calendar-o') + " " + title); +}); diff --git a/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 b/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 index a11f7419a..469873d22 100644 --- a/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 +++ b/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 @@ -1,33 +1,30 @@ export default Ember.Mixin.create({ _watchProps: function() { - var args = this.get('rerenderTriggers'); + const args = this.get('rerenderTriggers'); if (!Ember.isNone(args)) { - var self = this; - args.forEach(function(k) { - self.addObserver(k, self.rerenderString); - }); + args.forEach(k => this.addObserver(k, this.rerenderString)); } }.on('init'), - render: function(buffer) { + render(buffer) { this.renderString(buffer); }, - renderString: function(buffer){ - var template = Discourse.__container__.lookup('template:' + this.rawTemplate); + renderString(buffer){ + const template = Discourse.__container__.lookup('template:' + this.rawTemplate); if (template) { buffer.push(template(this)); } }, - _rerenderString: function() { - var buffer = []; + _rerenderString() { + const buffer = []; this.renderString(buffer); this.$().html(buffer.join('')); }, - rerenderString: function() { + rerenderString() { Ember.run.once(this, '_rerenderString'); } diff --git a/app/assets/javascripts/discourse/models/rest.js.es6 b/app/assets/javascripts/discourse/models/rest.js.es6 new file mode 100644 index 000000000..d85475644 --- /dev/null +++ b/app/assets/javascripts/discourse/models/rest.js.es6 @@ -0,0 +1,20 @@ +export default Ember.Object.extend({ + update(attrs) { + const self = this, + type = this.get('__type'); + return this.store.update(type, this.get('id'), attrs).then(function(result) { + if (result && result[type]) { + Object.keys(result).forEach(function(k) { + attrs[k] = result[k]; + }); + } + self.setProperties(attrs); + return result; + }); + }, + + destroyRecord() { + const type = this.get('__type'); + return this.store.destroyRecord(type, this); + } +}); diff --git a/app/assets/javascripts/discourse/models/result-set.js.es6 b/app/assets/javascripts/discourse/models/result-set.js.es6 new file mode 100644 index 000000000..210a83e4a --- /dev/null +++ b/app/assets/javascripts/discourse/models/result-set.js.es6 @@ -0,0 +1,22 @@ +export default Ember.ArrayProxy.extend({ + loading: false, + loadingMore: false, + totalRows: 0, + + loadMore() { + const loadMoreUrl = this.get('loadMoreUrl'); + if (!loadMoreUrl) { return; } + + const totalRows = this.get('totalRows'); + if (this.get('length') < totalRows && !this.get('loadingMore')) { + this.set('loadingMore', true); + + const self = this; + return this.store.appendResults(this, this.get('__type'), loadMoreUrl).then(function() { + self.set('loadingMore', false); + }); + } + + return Ember.RSVP.resolve(); + } +}); diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index a55d8849a..72f36db37 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -1,40 +1,49 @@ +import RestModel from 'discourse/models/rest'; +import ResultSet from 'discourse/models/result-set'; + const _identityMap = {}; -const RestModel = Ember.Object.extend({ - update(attrs) { - const self = this, - type = this.get('__type'); - return this.store.update(type, this.get('id'), attrs).then(function(result) { - if (result && result[type]) { - Object.keys(result).forEach(function(k) { - attrs[k] = result[k]; - }); - } - self.setProperties(attrs); - return result; - }); +export default Ember.Object.extend({ + pluralize(thing) { + return thing + "s"; }, - destroyRecord() { - const type = this.get('__type'); - return this.store.destroyRecord(type, this); - } -}); - -export default Ember.Object.extend({ findAll(type) { const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest'); const self = this; return adapter.findAll(this, type).then(function(result) { - return result[Ember.String.underscore(type + 's')].map(obj => self._hydrate(type, obj)); + return self._resultSet(type, result); }); }, - find(type, id) { + find(type, findArgs) { const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest'); const self = this; - return adapter.find(this, type, id).then(function(result) { - return self._hydrate(type, result[Ember.String.underscore(type)]); + return adapter.find(this, type, findArgs).then(function(result) { + if (typeof findArgs === "object") { + return self._resultSet(type, result); + } else { + return self._hydrate(type, result[Ember.String.underscore(type)]); + } + }); + }, + + appendResults(resultSet, type, url) { + const self = this; + + return Discourse.ajax(url).then(function(result) { + const typeName = Ember.String.underscore(self.pluralize(type)), + totalRows = result["total_rows_" + typeName] || result.get('totalRows'), + loadMoreUrl = result["load_more_" + typeName], + content = result[typeName].map(obj => self._hydrate(type, obj)); + + resultSet.setProperties({ totalRows, loadMoreUrl }); + resultSet.get('content').pushObjects(content); + + // If we've loaded them all, clear the load more URL + if (resultSet.get('length') >= totalRows) { + resultSet.set('loadMoreUrl', null); + } }); }, @@ -63,6 +72,15 @@ export default Ember.Object.extend({ }); }, + _resultSet(type, result) { + const typeName = Ember.String.underscore(this.pluralize(type)), + content = result[typeName].map(obj => this._hydrate(type, obj)), + totalRows = result["total_rows_" + typeName] || content.length, + loadMoreUrl = result["load_more_" + typeName]; + + return ResultSet.create({ content, totalRows, loadMoreUrl, store: this, __type: type }); + }, + _hydrate(type, obj) { if (!obj) { throw "Can't hydrate " + type + " of `null`"; } if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; } diff --git a/app/assets/javascripts/discourse/models/top-period.js.es6 b/app/assets/javascripts/discourse/models/top-period.js.es6 deleted file mode 100644 index ad4323b68..000000000 --- a/app/assets/javascripts/discourse/models/top-period.js.es6 +++ /dev/null @@ -1,32 +0,0 @@ -export default Ember.Object.extend({ - title: null, - - availablePeriods: function() { - var periods = this.get('periods'); - if (!periods) { return; } - - var self = this; - return periods.filter(function(p) { - return p !== self; - }); - }.property('showMoreUrl'), - - _createTitle: function() { - var id = this.get('id'); - if (id) { - var title = "this_week"; - if (id === "yearly") { - title = "this_year"; - } else if (id === "monthly") { - title = "this_month"; - } else if (id === "daily") { - title = "today"; - } else if (id === "all") { - title = "all"; - } - - this.set('title', I18n.t("filters.top." + title)); - } - }.on('init') - -}); diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index f73f132b1..e6adf8630 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -11,6 +11,10 @@ export default function() { }); this.resource('topicBySlug', { path: '/t/:slug' }); + this.resource('directory', function() { + this.route('show', {path: '/:period'}); + }); + this.resource('discovery', { path: '/' }, function() { // top this.route('top'); diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 index e3567ef85..8113104ea 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -67,14 +67,13 @@ export default function(filter, params) { setupController: function(controller, model) { var topics = this.get('topics'), - periods = this.controllerFor('discovery').get('periods'), periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''); this.controllerFor('navigation/category').set('canCreateTopic', topics.get('can_create_topic')); this.controllerFor('discovery/topics').setProperties({ model: topics, category: model, - period: periods.findBy('id', periodId), + period: periodId, selected: [], noSubcategories: params && !!params.no_subcategories, order: topics.get('params.order'), diff --git a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 index 84127c3a8..4b21e9b0b 100644 --- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 @@ -45,13 +45,11 @@ export default function(filter, extras) { }))); } - const periods = this.controllerFor('discovery').get('periods'), - periodId = model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''); - + const period = model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''); const topicOpts = { model, category: null, - period: periods.findBy('id', periodId), + period, selected: [], expandGloballyPinned: true }; diff --git a/app/assets/javascripts/discourse/routes/directory-index.js.es6 b/app/assets/javascripts/discourse/routes/directory-index.js.es6 new file mode 100644 index 000000000..cc8c80730 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/directory-index.js.es6 @@ -0,0 +1,6 @@ +export default Discourse.Route.extend({ + beforeModel: function() { + this.controllerFor('directory-show').setProperties({ sort: null, asc: null }); + this.replaceWith('directory.show', 'all'); + } +}); diff --git a/app/assets/javascripts/discourse/routes/directory-show.js.es6 b/app/assets/javascripts/discourse/routes/directory-show.js.es6 new file mode 100644 index 000000000..fc319f689 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/directory-show.js.es6 @@ -0,0 +1,31 @@ +export default Discourse.Route.extend({ + queryParams: { + order: { refreshModel: true }, + asc: { refreshModel: true }, + }, + + model(params) { + // If we refresh via `refreshModel` set the old model to loading + const existing = this.modelFor('directory-show'); + if (existing) { + existing.set('loading', true); + } + + this._period = params.period; + return this.store.find('directoryItem', { + id: params.period, + asc: params.asc, + order: params.order + }); + }, + + setupController(controller, model) { + controller.setProperties({ model, period: this._period }); + }, + + actions: { + changePeriod(period) { + this.transitionTo('directory.show', period); + } + } +}); diff --git a/app/assets/javascripts/discourse/templates/components/period-chooser.hbs b/app/assets/javascripts/discourse/templates/components/period-chooser.hbs new file mode 100644 index 000000000..8f7fddc94 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/period-chooser.hbs @@ -0,0 +1,11 @@ +

{{period-title period}}

+ + +
+ +
+
diff --git a/app/assets/javascripts/discourse/templates/components/top-period-buttons.hbs b/app/assets/javascripts/discourse/templates/components/top-period-buttons.hbs index 4b6c2ffab..169d69432 100644 --- a/app/assets/javascripts/discourse/templates/components/top-period-buttons.hbs +++ b/app/assets/javascripts/discourse/templates/components/top-period-buttons.hbs @@ -1,3 +1,5 @@ -{{#each p in period.availablePeriods}} - {{top-title-button period=p}} +{{#each p in periods}} + {{#d-button action="changePeriod" actionParam=p}} + {{period-title p}} + {{/d-button}} {{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/top-period-chooser.hbs b/app/assets/javascripts/discourse/templates/components/top-period-chooser.hbs deleted file mode 100644 index 53181310a..000000000 --- a/app/assets/javascripts/discourse/templates/components/top-period-chooser.hbs +++ /dev/null @@ -1,11 +0,0 @@ -{{top-title period=period}} - - -
- -
-
diff --git a/app/assets/javascripts/discourse/templates/directory.hbs b/app/assets/javascripts/discourse/templates/directory.hbs new file mode 100644 index 000000000..68207c460 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/directory.hbs @@ -0,0 +1,5 @@ +
+
+ {{outlet}} +
+
diff --git a/app/assets/javascripts/discourse/templates/directory/show.hbs b/app/assets/javascripts/discourse/templates/directory/show.hbs new file mode 100644 index 000000000..e06356f61 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/directory/show.hbs @@ -0,0 +1,50 @@ +{{period-chooser period=period action="changePeriod"}} + +{{#loading-spinner condition=model.loading}} + {{#if model.length}} + {{i18n "directory.total_rows" count=model.totalRows}} + + + + + {{directory-toggle field="likes_received" order=order asc=asc}} + {{directory-toggle field="likes_given" order=order asc=asc}} + {{directory-toggle field="topic_count" order=order asc=asc}} + {{directory-toggle field="post_count" order=order asc=asc}} + {{directory-toggle field="topics_entered" order=order asc=asc}} + {{#if showTimeRead}} + + {{/if}} + + + {{#each item in model}} + + + + + + + + {{#if showTimeRead}} + + {{/if}} + + {{/each}} + +
 {{i18n "directory.time_read"}}
+ {{avatar item imageSize="tiny"}} + {{#link-to 'user' item.username}}{{unbound item.username}}{{/link-to}} + {{number item.topic_count}}{{number item.post_count}}{{number item.topics_entered}}{{unbound item.time_read}}
+ + {{loading-spinner condition=model.loadingMore}} + {{else}} +
+

{{i18n "directory.no_results"}}

+ {{/if}} +{{/loading-spinner}} diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index 41c9ecd37..494da1abb 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -19,7 +19,7 @@
{{#if top}}
- {{top-period-chooser period=period}} + {{period-chooser period=period action="changePeriod"}}
{{/if}} {{#if topicTrackingState.hasIncoming}} @@ -73,7 +73,7 @@ {{#if top}}

{{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}} - {{top-period-buttons period=period}} + {{top-period-buttons period=period action="changePeriod"}}

{{else}}
diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs index 984a40198..feb13c5d4 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs @@ -1,7 +1,7 @@
{{#if top}}
- {{top-period-chooser period=period}} + {{period-chooser period=period action="changePeriod"}}
{{/if}} @@ -45,7 +45,7 @@

{{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}}
- {{top-period-buttons period=period}} + {{top-period-buttons period=period action="changePeriod"}}

{{else}}
diff --git a/app/assets/javascripts/discourse/templates/site-map.hbs b/app/assets/javascripts/discourse/templates/site-map.hbs index 34660f8f6..d4be2d7bd 100644 --- a/app/assets/javascripts/discourse/templates/site-map.hbs +++ b/app/assets/javascripts/discourse/templates/site-map.hbs @@ -22,6 +22,8 @@ {{/if}} +
  • {{#link-to 'directory'}}{{i18n "directory.title"}}{{/link-to}}
  • + {{plugin-outlet "site-map-links"}} {{#if showKeyboardShortcuts}} diff --git a/app/assets/javascripts/discourse/views/directory-show.js.es6 b/app/assets/javascripts/discourse/views/directory-show.js.es6 new file mode 100644 index 000000000..9fdb9eb2d --- /dev/null +++ b/app/assets/javascripts/discourse/views/directory-show.js.es6 @@ -0,0 +1,5 @@ +import LoadMore from 'discourse/mixins/load-more'; + +export default Discourse.View.extend(LoadMore, { + eyelineSelector: '.directory tbody tr' +}); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 489ed3c13..b318d01a1 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -29,7 +29,6 @@ //= require ./discourse/models/post-stream //= require ./discourse/models/topic-details //= require ./discourse/models/topic -//= require ./discourse/models/top-period //= require ./discourse/controllers/controller //= require ./discourse/controllers/discovery-sortable //= require ./discourse/controllers/object @@ -51,7 +50,6 @@ //= require ./discourse/routes/user-topic-list //= require ./discourse/routes/user-activity-stream //= require ./discourse/routes/topic-from-params -//= require ./discourse/components/top-title //= require ./discourse/components/text-field //= require ./discourse/components/visible //= require ./discourse/components/conditional-loading-spinner diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index e9b1645da..3ac9c951e 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -226,11 +226,12 @@ ol.category-breadcrumb { } .period-chooser { - + display: inline-block; @include unselectable; h2 { float: left; + margin: 5px 0 10px; } button { @@ -274,6 +275,10 @@ ol.category-breadcrumb { .top-title-buttons { display: inline; + + button { + margin-right: 0.5em; + } } div.education { diff --git a/app/assets/stylesheets/common/base/directory.scss b/app/assets/stylesheets/common/base/directory.scss new file mode 100644 index 000000000..d11141fbe --- /dev/null +++ b/app/assets/stylesheets/common/base/directory.scss @@ -0,0 +1,46 @@ +.directory { + margin-bottom: 100px; + + .period-chooser { + float: left; + } + .total-rows { + margin-top: 0.5em; + color: darken(scale-color-diff(), 20%); + float: right; + } + .spinner { + clear: both; + } + + table { + width: 100%; + margin-top: 1em; + margin-bottom: 1em; + + td, th { + padding: 0.5em; + text-align: left; + border-bottom: 1px solid scale-color-diff(); + } + + th.sortable { + cursor: pointer; + + width: 13%; + i.fa { + margin-left: 1em; + } + + &:hover { + background-color: scale-color-diff(); + } + } + + td.likes { + i { + color: $love; + } + } + } +} diff --git a/app/controllers/directory_controller.rb b/app/controllers/directory_controller.rb new file mode 100644 index 000000000..aea4bbc20 --- /dev/null +++ b/app/controllers/directory_controller.rb @@ -0,0 +1,8 @@ +class DirectoryController < ApplicationController + # This controller just exists to avoid 404s and to have the ember app load up + def index + end + + def show + end +end diff --git a/app/controllers/directory_items_controller.rb b/app/controllers/directory_items_controller.rb new file mode 100644 index 000000000..221149d22 --- /dev/null +++ b/app/controllers/directory_items_controller.rb @@ -0,0 +1,35 @@ +class DirectoryItemsController < ApplicationController + PAGE_SIZE = 50 + + def index + id = params.require(:id) + period_type = DirectoryItem.period_types[id.to_sym] + raise Discourse::InvalidAccess.new(:period_type) unless period_type + + result = DirectoryItem.where(period_type: period_type).includes(:user) + + order = params[:order] || DirectoryItem.headings.first + if DirectoryItem.headings.include?(order.to_sym) + dir = params[:asc] ? 'ASC' : 'DESC' + result = result.order("directory_items.#{order} #{dir}") + end + + if period_type == DirectoryItem.period_types[:all] + result = result.includes(:user_stat) + end + page = params[:page].to_i + result = result.order('users.username') + result_count = result.dup.count + result = result.limit(PAGE_SIZE).offset(PAGE_SIZE * page) + + serialized = serialize_data(result, DirectoryItemSerializer) + + more_params = params.slice(:id, :order, :asc) + more_params[:page] = page + 1 + + render_json_dump directory_items: serialized, + total_rows_directory_items: result_count, + load_more_directory_items: directory_items_path(more_params) + + end +end diff --git a/app/jobs/scheduled/directory_refresh.rb b/app/jobs/scheduled/directory_refresh.rb new file mode 100644 index 000000000..2809ac210 --- /dev/null +++ b/app/jobs/scheduled/directory_refresh.rb @@ -0,0 +1,9 @@ +module Jobs + class DirectoryRefresh < Jobs::Scheduled + every 1.hour + + def execute(args) + DirectoryItem.refresh! + end + end +end diff --git a/app/models/directory_item.rb b/app/models/directory_item.rb new file mode 100644 index 000000000..d2b8c02a5 --- /dev/null +++ b/app/models/directory_item.rb @@ -0,0 +1,57 @@ +class DirectoryItem < ActiveRecord::Base + belongs_to :user + has_one :user_stat, foreign_key: :user_id, primary_key: :user_id + + def self.headings + @headings ||= [:likes_received, + :likes_given, + :topics_entered, + :topic_count, + :post_count] + end + + def self.period_types + @types ||= Enum.new(:all, :yearly, :monthly, :weekly, :daily) + end + + def self.refresh! + ActiveRecord::Base.transaction do + exec_sql "TRUNCATE TABLE directory_items" + period_types.keys.each {|p| refresh_period!(p)} + end + end + + def self.refresh_period!(period_type) + since = case period_type + when :daily then 1.day.ago + when :weekly then 1.week.ago + when :monthly then 1.month.ago + when :yearly then 1.year.ago + else 1000.years.ago + end + + exec_sql "INSERT INTO directory_items + (period_type, user_id, likes_received, likes_given, topics_entered, topic_count, post_count) + SELECT + :period_type, + u.id, + SUM(CASE WHEN ua.action_type = :was_liked_type THEN 1 ELSE 0 END), + SUM(CASE WHEN ua.action_type = :like_type THEN 1 ELSE 0 END), + (SELECT COUNT(topic_id) FROM topic_views AS v WHERE v.user_id = u.id AND v.viewed_at > :since), + SUM(CASE WHEN ua.action_type = :new_topic_type THEN 1 ELSE 0 END), + SUM(CASE WHEN ua.action_type = :reply_type THEN 1 ELSE 0 END) + FROM users AS u + LEFT OUTER JOIN user_actions AS ua ON ua.user_id = u.id + WHERE u.active + AND NOT u.blocked + AND COALESCE(ua.created_at, :since) >= :since + AND u.id > 0 + GROUP BY u.id", + period_type: period_types[period_type], + since: since, + like_type: UserAction::LIKE, + was_liked_type: UserAction::WAS_LIKED, + new_topic_type: UserAction::NEW_TOPIC, + reply_type: UserAction::REPLY + end +end diff --git a/app/serializers/directory_item_serializer.rb b/app/serializers/directory_item_serializer.rb new file mode 100644 index 000000000..242196a83 --- /dev/null +++ b/app/serializers/directory_item_serializer.rb @@ -0,0 +1,35 @@ +class DirectoryItemSerializer < ApplicationSerializer + + attributes :id, + :username, + :uploaded_avatar_id, + :avatar_template, + :time_read + + attributes *DirectoryItem.headings + + def id + object.user_id + end + + def username + object.user.username + end + + def uploaded_avatar_id + object.user.uploaded_avatar_id + end + + def avatar_template + object.user.avatar_template + end + + def time_read + AgeWords.age_words(object.user_stat.time_read) + end + + def include_time_read? + object.period_type == DirectoryItem.period_types[:all] + end + +end diff --git a/app/serializers/directory_serializer.rb b/app/serializers/directory_serializer.rb new file mode 100644 index 000000000..415d9b7bf --- /dev/null +++ b/app/serializers/directory_serializer.rb @@ -0,0 +1,9 @@ +class DirectorySerializer < ApplicationSerializer + attributes :id + has_many :directory_items, serializer: DirectoryItemSerializer, embed: :objects + + def id + object.filter + end + +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 1b01d8f57..2320f3730 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -237,6 +237,19 @@ en: sent_by_user: "Sent by {{user}}" sent_by_you: "Sent by you" + directory: + title: "User Directory" + likes_given: "Likes Given" + likes_received: "Likes Received" + topics_entered: "Topics Entered" + time_read: "Time Read" + topic_count: "Topics" + post_count: "Replies" + no_results: "No results were found for this time period." + total_rows: + one: "1 user found" + other: "%{count} users found" + groups: visible: "Group is visible to all users" title: diff --git a/config/routes.rb b/config/routes.rb index 427a31960..a3cfd922b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -25,6 +25,9 @@ Discourse::Application.routes.draw do resources :about + resources :directory + resources :directory_items + get "site" => "site#site" namespace :site do get "settings" diff --git a/db/migrate/20150318143915_create_directory_items.rb b/db/migrate/20150318143915_create_directory_items.rb new file mode 100644 index 000000000..37f08336a --- /dev/null +++ b/db/migrate/20150318143915_create_directory_items.rb @@ -0,0 +1,16 @@ +class CreateDirectoryItems < ActiveRecord::Migration + def change + create_table :directory_items do |t| + t.integer :period_type, null: false + t.references :user, null: false + t.integer :likes_received, null: false + t.integer :likes_given, null: false + t.integer :topics_entered, null: false + t.integer :topic_count, null: false + t.integer :post_count, null: false + t.timestamps + end + + add_index :directory_items, :period_type + end +end diff --git a/spec/controllers/directory_items_controller_spec.rb b/spec/controllers/directory_items_controller_spec.rb new file mode 100644 index 000000000..bc36d9b86 --- /dev/null +++ b/spec/controllers/directory_items_controller_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe DirectoryItemsController do + + it "requires an `id` param" do + ->{ xhr :get, :index }.should raise_error + end + + it "requires a proper `id` param" do + xhr :get, :index, id: 'eviltrout' + response.should_not be_success + end + + context "with data" do + before do + Fabricate(:user) + DirectoryItem.refresh! + end + + it "succeeds with a valid value" do + xhr :get, :index, id: 'all' + response.should be_success + json = ::JSON.parse(response.body) + + json.should be_present + json['directory_items'].should be_present + json['total_rows_directory_items'].should be_present + json['load_more_directory_items'].should be_present + end + end +end diff --git a/spec/models/directory_item_spec.rb b/spec/models/directory_item_spec.rb new file mode 100644 index 000000000..624027c24 --- /dev/null +++ b/spec/models/directory_item_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe DirectoryItem do + context 'refresh' do + let!(:user) { Fabricate(:user) } + + it "creates the record for the user" do + DirectoryItem.refresh! + expect(DirectoryItem.where(period_type: DirectoryItem.period_types[:all]) + .where(user_id: user.id) + .exists?).to be_true + end + + end +end diff --git a/test/javascripts/fixtures/directory-fixtures.js.es6 b/test/javascripts/fixtures/directory-fixtures.js.es6 new file mode 100644 index 000000000..cb70d2aa7 --- /dev/null +++ b/test/javascripts/fixtures/directory-fixtures.js.es6 @@ -0,0 +1,3 @@ +export default { + "directory_items": {"directory_items":[{"id":32,"username":"codinghorror","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/codinghorror/{size}/2.png","time_read":"55d","likes_received":9370,"likes_given":7725,"topics_entered":11453,"topic_count":184,"post_count":12263},{"id":1,"username":"sam","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/sam/{size}/2.png","time_read":"52d","likes_received":7834,"likes_given":2693,"topics_entered":11024,"topic_count":276,"post_count":7802},{"id":19,"username":"eviltrout","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/eviltrout/{size}/2.png","time_read":"25d","likes_received":2383,"likes_given":319,"topics_entered":8041,"topic_count":34,"post_count":1602},{"id":6626,"username":"riking","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/riking/{size}/2.png","time_read":"17d","likes_received":2101,"likes_given":2756,"topics_entered":9055,"topic_count":163,"post_count":2548},{"id":1995,"username":"zogstrip","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/zogstrip/{size}/2.png","time_read":"32d","likes_received":1838,"likes_given":4588,"topics_entered":10823,"topic_count":16,"post_count":2050},{"id":8300,"username":"cpradio","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/cpradio/{size}/2.png","time_read":"11d","likes_received":1538,"likes_given":1001,"topics_entered":6121,"topic_count":111,"post_count":1430},{"id":2,"username":"neil","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/neil/{size}/2.png","time_read":"24d","likes_received":1238,"likes_given":684,"topics_entered":3250,"topic_count":27,"post_count":969},{"id":4263,"username":"mcwumbly","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/mcwumbly/{size}/2.png","time_read":"15d","likes_received":1223,"likes_given":1296,"topics_entered":5924,"topic_count":81,"post_count":1031},{"id":5351,"username":"erlend_sh","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/erlend_sh/{size}/2.png","time_read":"9d","likes_received":1115,"likes_given":747,"topics_entered":3260,"topic_count":154,"post_count":721},{"id":5559,"username":"downey","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/downey/{size}/2.png","time_read":"5d","likes_received":983,"likes_given":1713,"topics_entered":2995,"topic_count":131,"post_count":850},{"id":2770,"username":"awesomerobot","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/awesomerobot/{size}/2.png","time_read":"9d","likes_received":952,"likes_given":195,"topics_entered":2411,"topic_count":13,"post_count":402},{"id":9775,"username":"elberet","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/elberet/{size}/2.png","time_read":"7d","likes_received":930,"likes_given":159,"topics_entered":4077,"topic_count":28,"post_count":755},{"id":8222,"username":"techAPJ","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/techapj/{size}/2.png","time_read":"12d","likes_received":791,"likes_given":1005,"topics_entered":3691,"topic_count":43,"post_count":463},{"id":6060,"username":"lightyear","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/lightyear/{size}/2.png","time_read":"3d","likes_received":708,"likes_given":330,"topics_entered":1717,"topic_count":34,"post_count":312},{"id":8,"username":"geek","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/geek/{size}/2.png","time_read":"20d","likes_received":634,"likes_given":152,"topics_entered":920,"topic_count":48,"post_count":298},{"id":464,"username":"DeanMarkTaylor","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/deanmarktaylor/{size}/2.png","time_read":"10d","likes_received":578,"likes_given":299,"topics_entered":2976,"topic_count":116,"post_count":485},{"id":11160,"username":"boomzilla","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/boomzilla/{size}/2.png","time_read":"1d","likes_received":561,"likes_given":398,"topics_entered":822,"topic_count":23,"post_count":185},{"id":4457,"username":"Lee_Ars","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/lee_ars/{size}/2.png","time_read":"3d","likes_received":530,"likes_given":163,"topics_entered":2250,"topic_count":46,"post_count":327},{"id":8571,"username":"tobiaseigen","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/tobiaseigen/{size}/2.png","time_read":"6d","likes_received":524,"likes_given":1275,"topics_entered":2545,"topic_count":140,"post_count":435},{"id":3,"username":"supermathie","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/supermathie/{size}/2.png","time_read":"20d","likes_received":510,"likes_given":312,"topics_entered":1733,"topic_count":62,"post_count":438},{"id":8493,"username":"PJH","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/pjh/{size}/2.png","time_read":"3d","likes_received":458,"likes_given":96,"topics_entered":1219,"topic_count":74,"post_count":318},{"id":8617,"username":"Mittineague","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/mittineague/{size}/2.png","time_read":"16d","likes_received":415,"likes_given":291,"topics_entered":6662,"topic_count":22,"post_count":757},{"id":10778,"username":"Lid","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/lid/{size}/2.png","time_read":"8d","likes_received":398,"likes_given":296,"topics_entered":1771,"topic_count":82,"post_count":307},{"id":6548,"username":"michaeld","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/michaeld/{size}/2.png","time_read":"3d","likes_received":387,"likes_given":165,"topics_entered":1407,"topic_count":48,"post_count":330},{"id":471,"username":"BhaelOchon","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/bhaelochon/{size}/2.png","time_read":"22d","likes_received":386,"likes_given":765,"topics_entered":8051,"topic_count":55,"post_count":486},{"id":4881,"username":"gerhard","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/gerhard/{size}/2.png","time_read":"6d","likes_received":360,"likes_given":393,"topics_entered":3030,"topic_count":57,"post_count":250},{"id":5707,"username":"trident","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/trident/{size}/2.png","time_read":"6d","likes_received":344,"likes_given":181,"topics_entered":4905,"topic_count":2,"post_count":549},{"id":3987,"username":"Sander78","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/sander78/{size}/2.png","time_read":"3d","likes_received":326,"likes_given":276,"topics_entered":3613,"topic_count":94,"post_count":392},{"id":3415,"username":"radq","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/radq/{size}/2.png","time_read":"3d","likes_received":299,"likes_given":110,"topics_entered":2503,"topic_count":16,"post_count":157},{"id":2989,"username":"meglio","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/meglio/{size}/2.png","time_read":"3d","likes_received":280,"likes_given":436,"topics_entered":1086,"topic_count":198,"post_count":458},{"id":4,"username":"stienman","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/stienman/{size}/2.png","time_read":"15d","likes_received":276,"likes_given":100,"topics_entered":291,"topic_count":13,"post_count":132},{"id":10855,"username":"abarker","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/abarker/{size}/2.png","time_read":"1d","likes_received":270,"likes_given":131,"topics_entered":703,"topic_count":14,"post_count":77},{"id":9653,"username":"TechnoBear","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/technobear/{size}/2.png","time_read":"4d","likes_received":263,"likes_given":507,"topics_entered":2931,"topic_count":51,"post_count":220},{"id":7948,"username":"probus","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/probus/{size}/2.png","time_read":"6d","likes_received":261,"likes_given":71,"topics_entered":2399,"topic_count":51,"post_count":206},{"id":9741,"username":"chapel","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/chapel/{size}/2.png","time_read":"2d","likes_received":239,"likes_given":169,"topics_entered":1228,"topic_count":11,"post_count":167},{"id":8810,"username":"fantasticfears","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/fantasticfears/{size}/2.png","time_read":"4d","likes_received":213,"likes_given":184,"topics_entered":2161,"topic_count":29,"post_count":227},{"id":38,"username":"frandallfarmer","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/frandallfarmer/{size}/2.png","time_read":"19d","likes_received":212,"likes_given":104,"topics_entered":3169,"topic_count":6,"post_count":114},{"id":8085,"username":"watchmanmonitor","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/watchmanmonitor/{size}/2.png","time_read":"2d","likes_received":202,"likes_given":654,"topics_entered":1453,"topic_count":73,"post_count":278},{"id":8909,"username":"AdamCapriola","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/adamcapriola/{size}/2.png","time_read":"3d","likes_received":200,"likes_given":179,"topics_entered":1689,"topic_count":49,"post_count":169},{"id":14,"username":"clay","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/clay/{size}/2.png","time_read":"14d","likes_received":183,"likes_given":103,"topics_entered":780,"topic_count":24,"post_count":97},{"id":6613,"username":"haiku","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/haiku/{size}/2.png","time_read":"3d","likes_received":183,"likes_given":308,"topics_entered":1919,"topic_count":33,"post_count":188},{"id":13132,"username":"purldator","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/purldator/{size}/2.png","time_read":"4d","likes_received":178,"likes_given":685,"topics_entered":1891,"topic_count":20,"post_count":299},{"id":810,"username":"ChrisHanel","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/chrishanel/{size}/2.png","time_read":"16d","likes_received":169,"likes_given":42,"topics_entered":639,"topic_count":9,"post_count":94},{"id":2625,"username":"kpfleming","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/kpfleming/{size}/2.png","time_read":"10d","likes_received":165,"likes_given":288,"topics_entered":2539,"topic_count":15,"post_count":233},{"id":7717,"username":"lake54","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/lake54/{size}/2.png","time_read":"2d","likes_received":148,"likes_given":440,"topics_entered":1604,"topic_count":33,"post_count":194},{"id":8018,"username":"shivermetimbers","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/shivermetimbers/{size}/2.png","time_read":"15h","likes_received":139,"likes_given":40,"topics_entered":185,"topic_count":30,"post_count":181},{"id":2316,"username":"pakl","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/pakl/{size}/2.png","time_read":"14d","likes_received":135,"likes_given":198,"topics_entered":1034,"topic_count":46,"post_count":130},{"id":3681,"username":"Ajarn","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/ajarn/{size}/2.png","time_read":"3d","likes_received":126,"likes_given":664,"topics_entered":1893,"topic_count":46,"post_count":291},{"id":7229,"username":"DavidGNavas","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/davidgnavas/{size}/2.png","time_read":"4d","likes_received":124,"likes_given":210,"topics_entered":2032,"topic_count":17,"post_count":60},{"id":3062,"username":"Sailsman63","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/sailsman63/{size}/2.png","time_read":"8d","likes_received":124,"likes_given":139,"topics_entered":4257,"topic_count":10,"post_count":146}],"total_rows_directory_items":12546,"load_more_directory_items":"/directory_items?id=all&order=likes_received&page=1"} +}; diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index 06ebebbd7..cc51c3db0 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -7,6 +7,10 @@ function parsePostData(query) { return result; } +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + function response(code, obj) { if (typeof code === "object") { obj = code; @@ -24,6 +28,11 @@ const _widgets = [ {id: 124, name: 'Evil Repellant'} ]; +const _moreWidgets = [ + {id: 223, name: 'Bass Lure'}, + {id: 224, name: 'Good Repellant'} +]; + export default function() { const server = new Pretender(function() { @@ -101,12 +110,23 @@ export default function() { this.put('/widgets/:widget_id', function(request) { const w = _widgets.findBy('id', parseInt(request.params.widget_id)); - const cloned = JSON.parse(JSON.stringify(w)); - return response({ widget: cloned }); + return response({ widget: clone(w) }); }); - this.get('/widgets', function() { - return response({ widgets: _widgets }); + this.get('/widgets', function(request) { + let result = _widgets; + + const qp = request.queryParams; + if (qp) { + if (qp.name) { result = result.filterBy('name', qp.name); } + if (qp.id) { result = result.filterBy('id', parseInt(qp.id)); } + } + + return response({ widgets: result, total_rows_widgets: 4, load_more_widgets: '/load-more-widgets' }); + }); + + this.get('/load-more-widgets', function() { + return response({ widgets: _moreWidgets, total_rows_widgets: 4, load_more_widgets: '/load-more-widgets' }); }); this.delete('/widgets/:widget_id', success); diff --git a/test/javascripts/integration/directory-test.js.es6 b/test/javascripts/integration/directory-test.js.es6 new file mode 100644 index 000000000..84c3c3007 --- /dev/null +++ b/test/javascripts/integration/directory-test.js.es6 @@ -0,0 +1,8 @@ +integration("User Directory"); + +test("Visit Page", function() { + visit("/directory/all"); + andThen(function() { + ok(exists('.directory table tr'), "has a list of users"); + }); +}); diff --git a/test/javascripts/models/result-set-test.js.es6 b/test/javascripts/models/result-set-test.js.es6 new file mode 100644 index 000000000..7ce1f4ebf --- /dev/null +++ b/test/javascripts/models/result-set-test.js.es6 @@ -0,0 +1,28 @@ +module('result-set'); + +import ResultSet from 'discourse/models/result-set'; +import createStore from 'helpers/create-store'; + +test('defaults', function() { + const rs = ResultSet.create({ content: [] }); + equal(rs.get('length'), 0); + equal(rs.get('totalRows'), 0); + ok(!rs.get('loadMoreUrl')); + ok(!rs.get('loading')); + ok(!rs.get('loadingMore')); +}); + +test('pagination support', function() { + const store = createStore(); + store.findAll('widget').then(function(rs) { + equal(rs.get('length'), 2); + equal(rs.get('totalRows'), 4); + ok(rs.get('loadMoreUrl'), 'has a url to load more'); + + rs.loadMore().then(function() { + equal(rs.get('length'), 4); + ok(!rs.get('loadMoreUrl')); + }); + }); + +}); diff --git a/test/javascripts/models/store-test.js.es6 b/test/javascripts/models/store-test.js.es6 index 20f5cce30..d9e68fc78 100644 --- a/test/javascripts/models/store-test.js.es6 +++ b/test/javascripts/models/store-test.js.es6 @@ -19,7 +19,20 @@ test('find', function() { store.find('widget', 123).then(function(w2) { equal(w, w2); }); + }); +}); +test('find with object id', function() { + const store = createStore(); + store.find('widget', {id: 123}).then(function(w) { + equal(w.get('firstObject.name'), 'Trout Lure'); + }); +}); + +test('find with query param', function() { + const store = createStore(); + store.find('widget', {name: 'Trout Lure'}).then(function(w) { + equal(w.get('firstObject.id'), 123); }); }); @@ -33,7 +46,7 @@ test('update', function() { test('findAll', function() { const store = createStore(); store.findAll('widget').then(function(result) { - equal(result.length, 2); + equal(result.get('length'), 2); const w = result.findBy('id', 124); equal(w.get('name'), 'Evil Repellant'); });