FEATURE: better top pages

This commit is contained in:
Régis Hanol
2014-01-14 01:02:14 +01:00
parent 069a42cde1
commit 3a6bffa05d
36 changed files with 400 additions and 302 deletions

View File

@ -88,9 +88,8 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
}, },
loginRequired: function() { loginRequired: function() {
return ( return Discourse.SiteSettings.login_required &&
Discourse.SiteSettings.login_required && !Discourse.User.current() !Discourse.User.current();
);
}.property(), }.property(),
redirectIfLoginRequired: function(route) { redirectIfLoginRequired: function(route) {

View File

@ -22,10 +22,12 @@ Discourse.BasicTopicListComponent = Ember.Component.extend({
}.observes('topicList'), }.observes('topicList'),
_initFromTopicList: function(topicList) { _initFromTopicList: function(topicList) {
if (topicList !== null) {
this.setProperties({ this.setProperties({
topics: topicList.get('topics'), topics: topicList.get('topics'),
sortOrder: topicList.get('sortOrder') sortOrder: topicList.get('sortOrder')
}); });
}
}, },
init: function() { init: function() {

View File

@ -30,7 +30,7 @@ Discourse.ListCategoriesController = Discourse.ObjectController.extend({
latestTopicOnly: function() { latestTopicOnly: function() {
return this.get('categories').find(function(c) { return c.get('featuredTopics.length') > 1; }) === undefined; return this.get('categories').find(function(c) { return c.get('featuredTopics.length') > 1; }) === undefined;
}.property('categories.featuredTopics.length') }.property('categories.@each.featuredTopics.length')
}); });

View File

@ -95,12 +95,12 @@ Discourse.ListController = Discourse.Controller.extend({
// Put in the appropriate page title based on our view // Put in the appropriate page title based on our view
updateTitle: function() { updateTitle: function() {
if (this.get('filterMode') === 'categories') { if (this.get('filterMode') === 'categories') {
return Discourse.set('title', I18n.t('categories_list')); Discourse.set('title', I18n.t('categories_list'));
} else { } else {
if (this.present('category')) { if (this.present('category')) {
return Discourse.set('title', this.get('category.name').capitalize() + " " + I18n.t('topic.list')); Discourse.set('title', this.get('category.name').capitalize() + " " + I18n.t('topic.list'));
} else { } else {
return Discourse.set('title', I18n.t('topic.list')); Discourse.set('title', I18n.t('topic.list'));
} }
} }
}.observes('filterMode', 'category'), }.observes('filterMode', 'category'),
@ -134,5 +134,5 @@ Discourse.ListController = Discourse.Controller.extend({
}); });
Discourse.ListController.reopenClass({ Discourse.ListController.reopenClass({
filters: <%= Discourse.filters.map(&:to_s) %> FILTERS: <%= Discourse.filters.map(&:to_s) %>
}); });

View File

@ -23,12 +23,18 @@ Discourse.ListTopController = Discourse.ObjectController.extend({
return null; return null;
}.property(), }.property(),
showThisYear: function() { hasDisplayedAllTopLists: Em.computed.and('content.yearly', 'content.monthly', 'content.weekly', 'content.daily'),
if (Discourse.User.current()) {
return !Discourse.User.currentProp("hasBeenSeenInTheLastMonth"); showMoreUrl: function(period) {
} else { var url = "", category = this.get("category");
return true; if (category) { url += category.get("url") + "/l"; }
} url += "/top/" + period;
}.property() return url;
},
showMoreDailyUrl: function() { return this.showMoreUrl("daily"); }.property("category.url"),
showMoreWeeklyUrl: function() { return this.showMoreUrl("weekly"); }.property("category.url"),
showMoreMonthlyUrl: function() { return this.showMoreUrl("monthly"); }.property("category.url"),
showMoreYearlyUrl: function() { return this.showMoreUrl("yearly"); }.property("category.url"),
}); });

View File

@ -37,12 +37,10 @@ Discourse.StaticController = Discourse.Controller.extend({
}); });
Discourse.StaticController.reopenClass({ Discourse.StaticController.reopenClass({
pages: ['faq', 'tos', 'privacy', 'login'], PAGES: ['faq', 'tos', 'privacy', 'login'],
configs: { CONFIGS: {
'faq': 'faq_url', 'faq': 'faq_url',
'tos': 'tos_url', 'tos': 'tos_url',
'privacy': 'privacy_policy_url' 'privacy': 'privacy_policy_url'
} }
}); });

View File

@ -165,7 +165,7 @@ Discourse.URL = Em.Object.createWithMixins({
@param {String} path the path we're navigating to @param {String} path the path we're navigating to
**/ **/
navigatedToHome: function(oldPath, path) { navigatedToHome: function(oldPath, path) {
var defaultFilter = "/" + Discourse.ListController.filters[0]; var defaultFilter = "/" + Discourse.ListController.FILTERS[0];
if (path === "/" && (oldPath === "/" || oldPath === defaultFilter)) { if (path === "/" && (oldPath === "/" || oldPath === defaultFilter)) {
// Refresh our list // Refresh our list

View File

@ -44,7 +44,7 @@ Discourse.Category = Discourse.Model.extend({
}.property('url'), }.property('url'),
style: function() { style: function() {
return "background-color: #" + this.get('category.color') + "; color: #" + (this.get('category.text_color')) + ";"; return "background-color: #" + this.get('category.color') + "; color: #" + this.get('category.text_color') + ";";
}.property('color', 'text_color'), }.property('color', 'text_color'),
moreTopics: function() { moreTopics: function() {
@ -54,7 +54,7 @@ Discourse.Category = Discourse.Model.extend({
save: function() { save: function() {
var url = "/categories"; var url = "/categories";
if (this.get('id')) { if (this.get('id')) {
url = "/categories/" + (this.get('id')); url = "/categories/" + this.get('id');
} }
return Discourse.ajax(url, { return Discourse.ajax(url, {

View File

@ -55,31 +55,20 @@ Discourse.CategoryList.reopenClass({
return categories; return categories;
}, },
list: function(filter) { list: function() {
var self = this, var self = this;
finder = null;
if (filter === 'categories') { return PreloadStore.getAndRemove("categories_list", function() {
finder = PreloadStore.getAndRemove("categories_list", function() {
return Discourse.ajax("/categories.json"); return Discourse.ajax("/categories.json");
}); }).then(function(result) {
} else { return Discourse.CategoryList.create({
finder = Discourse.ajax("/" + filter + ".json"); categories: self.categoriesFrom(result),
}
return finder.then(function(result) {
var categoryList = Discourse.TopicList.create();
categoryList.setProperties({
can_create_category: result.category_list.can_create_category, can_create_category: result.category_list.can_create_category,
can_create_topic: result.category_list.can_create_topic, can_create_topic: result.category_list.can_create_topic,
categories: self.categoriesFrom(result),
draft_key: result.category_list.draft_key, draft_key: result.category_list.draft_key,
draft_sequence: result.category_list.draft_sequence draft_sequence: result.category_list.draft_sequence,
}); });
return categoryList;
}); });
} }
}); });

View File

@ -6,8 +6,6 @@
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
var validNavNames = <%= Discourse.top_menu_items.map(&:to_s) %>;
var validAnon = <%= Discourse.anonymous_top_menu_items.map(&:to_s) %>;
Discourse.NavItem = Discourse.Model.extend({ Discourse.NavItem = Discourse.Model.extend({
@ -69,26 +67,25 @@ Discourse.NavItem = Discourse.Model.extend({
}); });
Discourse.NavItem.reopenClass({ Discourse.NavItem.reopenClass({
NAMES: <%= Discourse.top_menu_items.map(&:to_s) %>,
ANONYMOUS_NAMES: <%= Discourse.anonymous_top_menu_items.map(&:to_s) %>,
// create a nav item from the text, will return null if there is not valid nav item for this particular text // create a nav item from the text, will return null if there is not valid nav item for this particular text
fromText: function(text, opts) { fromText: function(text, opts) {
var countSummary = opts.countSummary, var split = text.split(","),
split = text.split(","),
name = split[0], name = split[0],
testName = name.split("/")[0]; testName = name.split("/")[0];
if (!opts.loggedOn && !validAnon.contains(testName)) return null; if (!opts.loggedOn && !Discourse.NavItem.ANONYMOUS_NAMES.contains(testName)) return null;
if (!Discourse.Category.list() && testName === "categories") return null; if (!Discourse.Category.list() && testName === "categories") return null;
if (!validNavNames.contains(testName)) return null; if (!Discourse.NavItem.NAMES.contains(testName)) return null;
opts = { return Discourse.NavItem.create({
name: name, name: name,
hasIcon: name === "unread" || name === "starred", hasIcon: name === "unread" || name === "starred",
filters: split.splice(1), filters: split.splice(1),
category: opts.category category: opts.category,
}; });
return Discourse.NavItem.create(opts);
} }
}); });

View File

@ -1,34 +0,0 @@
/**
A data model representing a list of top topic lists
@class TopList
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.TopList = Discourse.Model.extend({});
Discourse.TopList.reopenClass({
find: function() {
return PreloadStore.getAndRemove("top_list", function() {
return Discourse.ajax("/top.json");
}).then(function (result) {
var topList = Discourse.TopList.create({
can_create_topic: result.can_create_topic,
yearly: Discourse.TopicList.from(result.yearly),
monthly: Discourse.TopicList.from(result.monthly),
weekly: Discourse.TopicList.from(result.weekly),
daily: Discourse.TopicList.from(result.daily)
});
// disable sorting
topList.setProperties({
"yearly.sortOrder": undefined,
"monthly.sortOrder": undefined,
"weekly.sortOrder": undefined,
"daily.sortOrder": undefined
});
return topList;
});
}
});

View File

@ -0,0 +1,37 @@
/**
A data model representing a list of top topic lists
@class TopList
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.TopList = Discourse.Model.extend({});
Discourse.TopList.reopenClass({
PERIODS: <%= TopTopic.periods.map(&:to_s) %>,
find: function(period, category) {
return PreloadStore.getAndRemove("top_lists", function() {
var url = "";
if (category) { url += category.get("url") + "/l"; }
url += "/top";
if (period) { url += "/" + period; }
return Discourse.ajax(url + ".json");
}).then(function (result) {
var topList = Discourse.TopList.create({});
_.each(Discourse.TopList.PERIODS, function(period) {
// if there is a list for that period
if (result[period]) {
// instanciate a new topic list with no sorting
topList.set(period, Discourse.TopicList.from(result[period]));
topList.set(period + ".sortOrder", undefined);
}
});
return topList;
});
}
});

View File

@ -65,8 +65,7 @@ Discourse.TopicList = Discourse.Model.extend({
params.sort_descending = sortOrder.get('descending'); params.sort_descending = sortOrder.get('descending');
this.set('loaded', false); this.set('loaded', false);
var finder = finderFor(this.get('filter'), params); finderFor(this.get('filter'), params).then(function (result) {
finder().then(function (result) {
var newTopics = Discourse.TopicList.topicsFrom(result), var newTopics = Discourse.TopicList.topicsFrom(result),
topics = self.get('topics'); topics = self.get('topics');

View File

@ -376,8 +376,6 @@ Discourse.User = Discourse.Model.extend({
}); });
Discourse.User.reopenClass(Discourse.Singleton, { Discourse.User.reopenClass(Discourse.Singleton, {
/** /**
Find a `Discourse.User` for a given username. Find a `Discourse.User` for a given username.

View File

@ -14,34 +14,48 @@ Discourse.Route.buildRoutes(function() {
}); });
// Generate static page routes // Generate static page routes
Discourse.StaticController.pages.forEach(function(p) { _.each(Discourse.StaticController.PAGES, function (page) {
router.route(p, { path: "/" + p }); router.route(page, { path: '/' + page });
}); });
// List routes this.resource("list", { path: "/" }, function() {
this.resource('list', { path: '/' }, function() {
router = this; router = this;
// Generate routes for all our filters // categories
Discourse.ListController.filters.forEach(function(filter) { this.route('categories');
router.route(filter, { path: "/" + filter });
router.route(filter, { path: "/" + filter + "/more" }); // top
router.route(filter + "Category", { path: "/category/:slug/l/" + filter }); this.route('top');
router.route(filter + "Category", { path: "/category/:slug/l/" + filter + "/more" }); this.route('topCategory', { path: '/category/:slug/l/top' });
router.route(filter + "Category", { path: "/category/:parentSlug/:slug/l/" + filter }); this.route('topCategoryNone', { path: '/category/:slug/none/l/top' });
router.route(filter + "Category", { path: "/category/:parentSlug/:slug/l/" + filter + "/more" }); this.route('topCategory', { path: '/category/:parentSlug/:slug/l/top' });
// top by periods
_.each(Discourse.TopList.PERIODS, function(period) {
var top = 'top' + period.capitalize();
router.route(top, { path: '/top/' + period });
router.route(top, { path: '/top/' + period + '/more' });
router.route(top + 'Category', { path: '/category/:slug/l/top/' + period });
router.route(top + 'Category', { path: '/category/:slug/l/top/' + period + '/more' });
router.route(top + 'CategoryNone', { path: '/category/:slug/none/l/top/' + period });
router.route(top + 'CategoryNone', { path: '/category/:slug/none/l/top/' + period + '/more' });
router.route(top + 'Category', { path: '/category/:parentSlug/:slug/l/top/' + period });
router.route(top + 'Category', { path: '/category/:parentSlug/:slug/l/top/' + period + '/more' });
}); });
// homepage // filters
var homepage = Discourse.User.current() ? _.each(Discourse.ListController.FILTERS, function(filter) {
Discourse.User.currentProp("homepage") : router.route(filter, { path: '/' + filter });
Discourse.Utilities.defaultHomepage(); router.route(filter, { path: '/' + filter + '/more' });
this.route(homepage, { path: '/' }); router.route(filter + 'Category', { path: '/category/:slug/l/' + filter });
router.route(filter + 'Category', { path: '/category/:slug/l/' + filter + '/more' });
router.route(filter + 'CategoryNone', { path: '/category/:slug/none/l/' + filter });
router.route(filter + 'CategoryNone', { path: '/category/:slug/none/l/' + filter + '/more' });
router.route(filter + 'Category', { path: '/category/:parentSlug/:slug/l/' + filter });
router.route(filter + 'Category', { path: '/category/:parentSlug/:slug/l/' + filter + '/more' });
});
// categories page // default filter for a category
this.route('categories', { path: '/categories' });
// category
this.route('category', { path: '/category/:slug' }); this.route('category', { path: '/category/:slug' });
this.route('category', { path: '/category/:slug/more' }); this.route('category', { path: '/category/:slug/more' });
this.route('categoryNone', { path: '/category/:slug/none' }); this.route('categoryNone', { path: '/category/:slug/none' });
@ -49,33 +63,31 @@ Discourse.Route.buildRoutes(function() {
this.route('category', { path: '/category/:parentSlug/:slug' }); this.route('category', { path: '/category/:parentSlug/:slug' });
this.route('category', { path: '/category/:parentSlug/:slug/more' }); this.route('category', { path: '/category/:parentSlug/:slug/more' });
// top page // homepage
this.route('top', { path: '/top' }); var homepage = Discourse.User.current() ? Discourse.User.currentProp('homepage') : Discourse.Utilities.defaultHomepage();
this.route(homepage, { path: '/' });
}); });
// User routes // User routes
this.resource('user', { path: '/users/:username' }, function() { this.resource('user', { path: '/users/:username' }, function() {
this.route('index', { path: '/'} );
this.resource('userActivity', { path: '/activity' }, function() { this.resource('userActivity', { path: '/activity' }, function() {
var self = this; router = this;
Object.keys(Discourse.UserAction.TYPES).forEach(function (userAction) { _.map(Discourse.UserAction.TYPES, function (id, userAction) {
self.route(userAction, { path: userAction.replace("_", "-") }); router.route(userAction, { path: userAction.replace('_', '-') });
}); });
}); });
this.resource('userPrivateMessages', { path: '/private-messages' }, function() { this.resource('userPrivateMessages', { path: '/private-messages' }, function() {
this.route('mine', { path: '/mine' }); this.route('mine');
this.route('unread', { path: '/unread' }); this.route('unread');
}); });
this.resource('preferences', { path: '/preferences' }, function() { this.resource('preferences', function() {
this.route('username', { path: '/username' }); this.route('username');
this.route('email', { path: '/email' }); this.route('email');
this.route('about', { path: '/about-me' }); this.route('about', { path: '/about-me' });
this.route('avatar', { path: '/avatar' });
}); });
this.route('invited', { path: 'invited' }); this.route('invited');
}); });
}); });

View File

@ -32,7 +32,7 @@ Discourse.Route = Em.Route.extend({
Discourse.set('notifyCount',0); Discourse.set('notifyCount',0);
var hideDropDownFunction = $('html').data('hide-dropdown'); var hideDropDownFunction = $('html').data('hide-dropdown');
if (hideDropDownFunction) return hideDropDownFunction(); if (hideDropDownFunction) { hideDropDownFunction(); }
} }
}); });

View File

@ -10,21 +10,8 @@ Discourse.FilteredListRoute = Discourse.Route.extend({
redirect: function() { Discourse.redirectIfLoginRequired(this); }, redirect: function() { Discourse.redirectIfLoginRequired(this); },
deactivate: function() {
this._super();
this.controllerFor('list').setProperties({
canCreateTopic: false,
filterMode: ''
});
},
renderTemplate: function() { renderTemplate: function() {
this.render('listTopics', { this.render('listTopics', { into: 'list', outlet: 'listView', controller: 'listTopics' });
into: 'list',
outlet: 'listView',
controller: 'listTopics'
});
}, },
setupController: function() { setupController: function() {
@ -44,7 +31,16 @@ Discourse.FilteredListRoute = Discourse.Route.extend({
listTopicsController.set('model', topicList); listTopicsController.set('model', topicList);
Discourse.FilteredListRoute.scrollToLastPosition(); Discourse.FilteredListRoute.scrollToLastPosition();
}); });
} },
deactivate: function() {
this._super();
this.controllerFor('list').setProperties({
canCreateTopic: false,
filterMode: ''
});
},
}); });
Discourse.FilteredListRoute.reopenClass({ Discourse.FilteredListRoute.reopenClass({
@ -57,6 +53,10 @@ Discourse.FilteredListRoute.reopenClass({
} }
}); });
Discourse.ListController.filters.forEach(function(filter) { _.each(Discourse.ListController.FILTERS, function(filter) {
Discourse["List" + (filter.capitalize()) + "Route"] = Discourse.FilteredListRoute.extend({ filter: filter }); Discourse["List" + filter.capitalize() + "Route"] = Discourse.FilteredListRoute.extend({ filter: filter });
});
_.each(Discourse.TopList.PERIODS, function(period) {
Discourse["ListTop" + period.capitalize() + "Route"] = Discourse.FilteredListRoute.extend({ filter: "top/" + period });
}); });

View File

@ -8,34 +8,18 @@
**/ **/
Discourse.ListCategoriesRoute = Discourse.Route.extend({ Discourse.ListCategoriesRoute = Discourse.Route.extend({
template: 'listCategories',
redirect: function() { Discourse.redirectIfLoginRequired(this); },
actions: {
createCategory: function() {
Discourse.Route.showModal(this, 'editCategory', Discourse.Category.create({
color: 'AB9364', text_color: 'FFFFFF', hotness: 5, group_permissions: [{group_name: "everyone", permission_type: 1}],
available_groups: Discourse.Site.current().group_names
}));
this.controllerFor('editCategory').set('selectedTab', 'general');
}
},
model: function() { model: function() {
var listTopicsController = this.controllerFor('listTopics'); this.controllerFor('listTop').set('content', null);
if (listTopicsController) { listTopicsController.set('content', null); } this.controllerFor('listTopics').set('content', null);
return this.controllerFor('list').load('categories'); return this.controllerFor('list').load('categories');
}, },
deactivate: function() { activate: function() {
this._super(); this._super();
this.controllerFor('list').set('canCreateCategory', false); this.controllerFor('list').setProperties({ filterMode: 'categories', category: null });
}, },
renderTemplate: function() { redirect: function() { Discourse.redirectIfLoginRequired(this); },
this.render(this.get('template'), { into: 'list', outlet: 'listView' });
},
afterModel: function(categoryList) { afterModel: function(categoryList) {
this.controllerFor('list').setProperties({ this.controllerFor('list').setProperties({
@ -44,13 +28,23 @@ Discourse.ListCategoriesRoute = Discourse.Route.extend({
}); });
}, },
activate: function() { renderTemplate: function() {
this.controllerFor('list').setProperties({ this.render('listCategories', { into: 'list', outlet: 'listView' });
filterMode: 'categories', },
category: null
}); deactivate: function() {
this._super();
this.controllerFor('list').set('canCreateCategory', false);
},
actions: {
createCategory: function() {
Discourse.Route.showModal(this, 'editCategory', Discourse.Category.create({
color: 'AB9364', text_color: 'FFFFFF', hotness: 5, group_permissions: [{group_name: 'everyone', permission_type: 1}],
available_groups: Discourse.Site.current().group_names
}));
this.controllerFor('editCategory').set('selectedTab', 'general');
} }
},
}); });

View File

@ -7,8 +7,17 @@
@module Discourse @module Discourse
**/ **/
Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({ Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
model: function(params) { model: function(params) {
return Discourse.Category.findBySlug(Em.get(params, 'slug'), Em.get(params, 'parentSlug')); this.controllerFor('listTop').set('content', null);
this.controllerFor('listCategories').set('content', null);
return Discourse.Category.findBySlug(params.slug, params.parentSlug);
},
activate: function() {
this._super();
// Add a search context
this.controllerFor('search').set('searchContext', this.modelFor(this.get('routeName')).get('searchContext'));
}, },
setupController: function(controller, category) { setupController: function(controller, category) {
@ -37,33 +46,29 @@ Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
canCreateTopic: topicList.get('can_create_topic'), canCreateTopic: topicList.get('can_create_topic'),
category: category category: category
}); });
self.controllerFor('listTopics').set('content', topicList); self.controllerFor('listTopics').setProperties({
self.controllerFor('listTopics').set('category', category); content: topicList,
category: category
});
Discourse.FilteredListRoute.scrollToLastPosition(); Discourse.FilteredListRoute.scrollToLastPosition();
}); });
}, },
activate: function() {
this._super();
// Add a search context
this.controllerFor('search').set('searchContext', this.modelFor(this.get('routeName')).get('searchContext'));
},
deactivate: function() { deactivate: function() {
this._super(); this._super();
// Clear the search context // Clear the search context
this.controllerFor('search').set('searchContext', null); this.controllerFor('search').set('searchContext', null);
} }
}); });
Discourse.ListCategoryNoneRoute = Discourse.ListCategoryRoute.extend({ Discourse.ListCategoryNoneRoute = Discourse.ListCategoryRoute.extend({ noSubcategories: true });
noSubcategories: true
});
Discourse.ListController.filters.forEach(function(filter) { _.each(Discourse.ListController.FILTERS, function(filter) {
Discourse["List" + filter.capitalize() + "CategoryRoute"] = Discourse.ListCategoryRoute.extend({ filter: filter }); Discourse["List" + filter.capitalize() + "CategoryRoute"] = Discourse.ListCategoryRoute.extend({ filter: filter });
Discourse["List" + filter.capitalize() + "CategoryNoneRoute"] = Discourse.ListCategoryRoute.extend({ filter: filter, noSubcategories: true });
}); });
_.each(Discourse.TopList.PERIODS, function(period) {
Discourse["ListTop" + period.capitalize() + "CategoryRoute"] = Discourse.ListCategoryRoute.extend({ filter: "top/" + period });
Discourse["ListTop" + period.capitalize() + "CategoryNoneRoute"] = Discourse.ListCategoryRoute.extend({ filter: "top/" + period, noSubcategories: true });
});

View File

@ -1,26 +1,42 @@
Discourse.ListTopRoute = Discourse.Route.extend({ Discourse.ListTopRoute = Discourse.Route.extend({
model: function() { model: function(params) {
return Discourse.TopList.find(); this.controllerFor('listCategories').set('content', null);
this.controllerFor('listTopics').set('content', null);
this.controllerFor('list').set('loading', true);
var category = Discourse.Category.findBySlug(params.slug, params.parentSlug);
if (category) { this.set('category', category); }
return Discourse.TopList.find(this.period, category);
}, },
activate: function() { activate: function() {
this._super(); this._super();
// will mark the "top" navigation item as selected this.controllerFor('list').setProperties({ filterMode: 'top', category: null });
this.controllerFor('list').setProperties({ },
filterMode: 'top',
category: null redirect: function() { Discourse.redirectIfLoginRequired(this); },
});
setupController: function(controller, model) {
var category = this.get('category'),
categorySlug = Discourse.Category.slugFor(category),
url = category === undefined ? 'top' : 'category/' + categorySlug + '/l/top';
this.controllerFor('listTop').setProperties({ content: model, category: category });
this.controllerFor('list').setProperties({ loading: false, filterMode: url });
if (category !== undefined) {
this.controllerFor('list').set('category', category);
}
Discourse.set('title', I18n.t('filters.top.title'));
}, },
renderTemplate: function() { renderTemplate: function() {
this.render('top', { into: 'list', outlet: 'listView' }); this.render('listTop', { into: 'list', outlet: 'listView' });
},
deactivate: function() {
this._super();
// Clear any filters when we leave the route
Discourse.URL.set('queryParams', null);
} }
}); });
Discourse.ListTopCategoryRoute = Discourse.ListTopRoute.extend({});

View File

@ -6,16 +6,16 @@
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.StaticController.pages.forEach(function(page) { _.each(Discourse.StaticController.PAGES, function(page) {
Discourse[(page.capitalize()) + "Route"] = Discourse.Route.extend({ Discourse[page.capitalize() + "Route"] = Discourse.Route.extend({
renderTemplate: function() { renderTemplate: function() {
this.render('static'); this.render('static');
}, },
setupController: function() { setupController: function() {
var config_key = Discourse.StaticController.configs[page]; var config_key = Discourse.StaticController.CONFIGS[page];
if (config_key && Discourse.SiteSettings[config_key].length > 0) { if (config_key && Discourse.SiteSettings[config_key].length > 0) {
Discourse.URL.redirectTo(Discourse.SiteSettings[config_key]); Discourse.URL.redirectTo(Discourse.SiteSettings[config_key]);
} else { } else {
@ -26,5 +26,3 @@ Discourse.StaticController.pages.forEach(function(page) {
}); });
}); });

View File

@ -32,7 +32,6 @@ Discourse.UserPrivateMessagesIndexRoute = createPMRoute('index', 'private-messag
Discourse.UserPrivateMessagesMineRoute = createPMRoute('mine', 'private-messages-sent'); Discourse.UserPrivateMessagesMineRoute = createPMRoute('mine', 'private-messages-sent');
Discourse.UserPrivateMessagesUnreadRoute = createPMRoute('unread', 'private-messages-unread'); Discourse.UserPrivateMessagesUnreadRoute = createPMRoute('unread', 'private-messages-unread');
Discourse.UserActivityTopicsRoute = Discourse.UserTopicListRoute.extend({ Discourse.UserActivityTopicsRoute = Discourse.UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.topics, userActionType: Discourse.UserAction.TYPES.topics,

View File

@ -37,9 +37,11 @@
<a href="{{unbound topic.lastUnreadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='fa fa-asterisk'></i></a> <a href="{{unbound topic.lastUnreadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='fa fa-asterisk'></i></a>
{{/if}} {{/if}}
</td> </td>
<td class="category"> <td class="category">
{{categoryLink topic.category}} {{categoryLink topic.category}}
</td> </td>
<td class='num posts'><a href="{{unbound topic.lastUnreadUrl}}" class='badge-posts'>{{number topic.posts_count numberKey="posts_long"}}</a></td> <td class='num posts'><a href="{{unbound topic.lastUnreadUrl}}" class='badge-posts'>{{number topic.posts_count numberKey="posts_long"}}</a></td>
<td class='num likes'> <td class='num likes'>

View File

@ -1,5 +1,4 @@
<div class='contents'> <div class='contents'>
<table id='topic-list' class='categories'> <table id='topic-list' class='categories'>
<thead> <thead>
<tr> <tr>
@ -7,9 +6,7 @@
<th class='latest'>{{i18n categories.latest}}</th> <th class='latest'>{{i18n categories.latest}}</th>
<th class='stats topics'>{{i18n categories.topics}}</th> <th class='stats topics'>{{i18n categories.topics}}</th>
<th class='stats posts'>{{i18n categories.posts}} <th class='stats posts'>{{i18n categories.posts}}
{{#if canEdit}} {{#if canEdit}}<button title='{{i18n categories.toggle_ordering}}' class='btn toggle-admin no-text' {{action toggleOrdering}}><i class='fa fa-wrench'></i></button>{{/if}}
<button title='{{i18n categories.toggle_ordering}}' class='btn toggle-admin no-text' {{action toggleOrdering}}><i class='fa fa-wrench'></i></button>
{{/if}}
</th> </th>
</tr> </tr>
</thead> </thead>
@ -19,9 +16,7 @@
<td class='category'> <td class='category'>
<div> <div>
<div class="pull-left"> <div class="pull-left">
{{#if controller.ordering}} {{#if controller.ordering}}<i class="fa fa-bars"></i>{{/if}}
<i class="fa fa-bars"></i>
{{/if}}
{{categoryLink this allowUncategorized=true}} {{categoryLink this allowUncategorized=true}}
{{#if unreadTopics}} {{#if unreadTopics}}
<a href={{unbound unreadUrl}} class='badge new-posts badge-notification' title='{{i18n topic.unread_topics count="unreadTopics"}}'>{{unbound unreadTopics}}</a> <a href={{unbound unreadUrl}} class='badge new-posts badge-notification' title='{{i18n topic.unread_topics count="unreadTopics"}}'>{{unbound unreadTopics}}</a>
@ -42,7 +37,6 @@
{{{description_excerpt}}} {{{description_excerpt}}}
</div> </div>
{{/if}} {{/if}}
{{#if subcategories}} {{#if subcategories}}
<div class='subcategories'> <div class='subcategories'>
{{i18n categories.subcategories}} {{i18n categories.subcategories}}
@ -102,10 +96,7 @@
</tr> </tr>
{{/each}} {{/each}}
</tbody> </tbody>
</table> </table>
</div> </div>
<footer id='topic-list-bottom'></footer>
<footer id='topic-list-bottom'>
</footer>

View File

@ -0,0 +1,48 @@
<div class="top-lists">
{{#if redirectedToTopPageReason}}
<div class="alert alert-info">
{{redirectedToTopPageReason}}
</div>
{{/if}}
{{#if content.yearly}}
<div class="clearfix">
<h2>{{i18n filters.top.this_year}}</h2>
{{basic-topic-list topicList=content.yearly}}
{{#if content.yearly.topics.length}}<a href={{unbound showMoreYearlyUrl}} class='btn pull-right'>{{i18n show_more}}</a>{{/if}}
</div>
{{/if}}
{{#if content.monthly}}
<div class="clearfix">
<h2>{{i18n filters.top.this_month}}</h2>
{{basic-topic-list topicList=content.monthly}}
{{#if content.monthly.topics.length}}<a href={{unbound showMoreMonthlyUrl}} class='btn pull-right'>{{i18n show_more}}</a>{{/if}}
</div>
{{/if}}
{{#if content.weekly}}
<div class="clearfix">
<h2>{{i18n filters.top.this_week}}</h2>
{{basic-topic-list topicList=content.weekly}}
{{#if content.weekly.topics.length}}<a href={{unbound showMoreWeeklyUrl}} class='btn pull-right'>{{i18n show_more}}</a>{{/if}}
</div>
{{/if}}
{{#if content.daily}}
<div class="clearfix">
<h2>{{i18n filters.top.today}}</h2>
{{basic-topic-list topicList=content.daily}}
{{#if content.daily.topics.length}}<a href={{unbound showMoreDailyUrl}} class='btn pull-right'>{{i18n show_more}}</a>{{/if}}
</div>
{{/if}}
<footer id="topic-list-bottom">
<h3>
{{#if hasDisplayedAllTopLists}}
{{#link-to "list.categories"}}{{i18n topic.browse_all_categories}}{{/link-to}} {{i18n or}} {{#link-to 'list.latest'}}{{i18n topic.view_latest_topics}}{{/link-to}}.
{{else}}
{{#link-to "list.categories"}}{{i18n topic.browse_all_categories}}{{/link-to}}, {{#link-to 'list.latest'}}{{i18n topic.view_latest_topics}}{{/link-to}} {{i18n or}} {{i18n filters.top.other_periods}}
{{#unless content.yearly}}<a href={{unbound showMoreYearlyUrl}} class='btn'>{{i18n filters.top.this_year}}</a>{{/unless}}
{{#unless content.monthly}}<a href={{unbound showMoreMonthlyUrl}} class='btn'>{{i18n filters.top.this_month}}</a>{{/unless}}
{{#unless content.weekly}}<a href={{unbound showMoreWeeklyUrl}} class='btn'>{{i18n filters.top.this_week}}</a>{{/unless}}
{{#unless content.daily}}<a href={{unbound showMoreDailyUrl}} class='btn'>{{i18n filters.top.today}}</a>{{/unless}}
{{/if}}
</h3>
</footer>
</div>

View File

@ -1,18 +0,0 @@
{{#if redirectedToTopPageReason}}
<div class="alert alert-info">
{{redirectedToTopPageReason}}
</div>
{{/if}}
{{#if showThisYear}}
<h2>{{i18n filters.top.this_year}}</h2>
{{basic-topic-list topicList=content.yearly}}
{{/if}}
<h2>{{i18n filters.top.this_month}}</h2>
{{basic-topic-list topicList=content.monthly}}
<h2>{{i18n filters.top.this_week}}</h2>
{{basic-topic-list topicList=content.weekly}}
<h2>{{i18n filters.top.today}}</h2>
{{basic-topic-list topicList=content.daily}}
<footer id="topic-list-bottom">
<h3>{{#link-to "list.categories"}}{{i18n topic.browse_all_categories}}{{/link-to}} {{i18n or}} {{#link-to 'list.latest'}}{{i18n topic.view_latest_topics}}{{/link-to}}</h3>
</footer>

View File

@ -0,0 +1,18 @@
/**
This view handles the rendering of the top lists
@class ListTopView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
Discourse.ListTopView = Discourse.View.extend({
didInsertElement: function() {
this._super();
Em.run.schedule('afterRender', function() {
$('html, body').scrollTop(0);
});
},
});

View File

@ -25,7 +25,7 @@ Discourse.NavItemView = Discourse.View.extend({
name = "category"; name = "category";
} }
return I18n.t("filters." + name + ".help", extra); return I18n.t("filters." + name + ".help", extra);
}.property("content.filter"), }.property("content.name"),
name: function() { name: function() {

View File

@ -63,6 +63,7 @@
width: 100%; width: 100%;
border-collapse: separate; border-collapse: separate;
border-spacing: 0; border-spacing: 0;
margin: 0 0 10px;
a.title:visited:not(.badge-notification) {color: #888;} a.title:visited:not(.badge-notification) {color: #888;}
> tbody > tr { > tbody > tr {
@ -230,6 +231,7 @@
} }
} }
#list-area .top-lists h2 { margin: 5px 0 10px; }
#topic-list tbody tr.has-excerpt .star { #topic-list tbody tr.has-excerpt .star {
vertical-align: top; vertical-align: top;

View File

@ -1,6 +1,15 @@
class ListController < ApplicationController class ListController < ApplicationController
before_filter :ensure_logged_in, except: [:latest, :hot, :category, :top, :category_feed, :latest_feed, :hot_feed, :topics_by] before_filter :ensure_logged_in, except: [
:topics_by,
# anonymous filters
Discourse.anonymous_filters, Discourse.anonymous_filters.map { |f| "#{f}_feed".to_sym },
# category
:category, :category_feed,
# top
:top_lists, TopTopic.periods.map { |p| "top_#{p}".to_sym }
].flatten
before_filter :set_category, only: [:category, :category_feed] before_filter :set_category, only: [:category, :category_feed]
skip_before_filter :check_xhr skip_before_filter :check_xhr
@ -72,13 +81,15 @@ class ListController < ApplicationController
redirect_to latest_path, :status => 301 redirect_to latest_path, :status => 301
end end
def top def top_lists
discourse_expires_in 1.minute
top = generate_top_lists top = generate_top_lists
respond_to do |format| respond_to do |format|
format.html do format.html do
@top = top @top = top
store_preloaded('top_list', MultiJson.dump(TopListSerializer.new(top, scope: guardian, root: false))) store_preloaded('top_lists', MultiJson.dump(TopListSerializer.new(top, scope: guardian, root: false)))
render 'top' render 'top'
end end
format.json do format.json do
@ -87,6 +98,16 @@ class ListController < ApplicationController
end end
end end
TopTopic.periods.each do |period|
define_method("top_#{period}") do
options = build_topic_list_options
user = list_target_user
list = TopicQuery.new(user, options).public_send("list_top_#{period}")
list.more_topics_url = construct_url_with(period, options, "top")
respond(list)
end
end
protected protected
def category_response(extra_opts=nil) def category_response(extra_opts=nil)
@ -181,19 +202,27 @@ class ListController < ApplicationController
def generate_top_lists def generate_top_lists
top = {} top = {}
topic_ids = Set.new
TopTopic.periods.each do |period|
options = { options = {
per_page: SiteSetting.topics_per_period_in_summary, per_page: SiteSetting.topics_per_period_in_summary,
except_topic_ids: topic_ids.to_a category: params[:category]
} }
list = TopicQuery.new(current_user, options).list_top_for(period) topic_query = TopicQuery.new(current_user, options)
topic_ids.merge(list.topic_ids) periods = periods_since(current_user.try(:last_seen_at))
top[period] = list
end periods.each { |period| top[period] = topic_query.list_top_for(period) }
top top
end end
def periods_since(date)
date ||= 1.year.ago
periods = [:daily]
periods << :weekly if date < 8.days.ago
periods << :monthly if date < 35.days.ago
periods << :yearly if date < 180.days.ago
periods
end
end end

View File

@ -1,19 +1,12 @@
class TopListSerializer < ApplicationSerializer class TopListSerializer < ApplicationSerializer
attributes :can_create_topic,
:yearly,
:monthly,
:weekly,
:daily
def can_create_topic
scope.can_create?(Topic)
end
TopTopic.periods.each do |period| TopTopic.periods.each do |period|
attribute period
define_method(period) do define_method(period) do
TopicListSerializer.new(object[period], scope: scope).as_json TopicListSerializer.new(object[period], scope: scope).as_json if object[period]
end end
end end
end end

View File

@ -604,6 +604,7 @@ en:
latest: "There are no latest topics. That's sad." latest: "There are no latest topics. That's sad."
hot: "There are no hot topics." hot: "There are no hot topics."
category: "There are no {{category}} topics." category: "There are no {{category}} topics."
top: "There are no top topics."
bottom: bottom:
latest: "There are no more latest topics." latest: "There are no more latest topics."
hot: "There are no more hot topics." hot: "There are no more hot topics."
@ -613,6 +614,7 @@ en:
unread: "There are no more unread topics." unread: "There are no more unread topics."
starred: "There are no more starred topics." starred: "There are no more starred topics."
category: "There are no more {{category}} topics." category: "There are no more {{category}} topics."
top: "There are no more top topics."
rank_details: rank_details:
toggle: toggle topic rank details toggle: toggle topic rank details
@ -1122,6 +1124,7 @@ en:
this_month: "This month" this_month: "This month"
this_week: "This week" this_week: "This week"
today: "Today" today: "Today"
other_periods: "dig into other periods"
redirect_reasons: redirect_reasons:
new_user: "Welcome! As a new visitor, we thought you might like to start with this list of the top discussion topics in the last year." new_user: "Welcome! As a new visitor, we thought you might like to start with this list of the top discussion topics in the last year."
not_seen_in_a_month: "Welcome back! We haven't seen you in a while. These are the top discussion topics since you've been away." not_seen_in_a_month: "Welcome back! We haven't seen you in a while. These are the top discussion topics since you've been away."

View File

@ -106,7 +106,6 @@ Discourse::Application.routes.draw do
get "email/unsubscribe/:key" => "email#unsubscribe", as: "email_unsubscribe" get "email/unsubscribe/:key" => "email#unsubscribe", as: "email_unsubscribe"
post "email/resubscribe/:key" => "email#resubscribe", as: "email_resubscribe" post "email/resubscribe/:key" => "email#resubscribe", as: "email_resubscribe"
resources :session, id: USERNAME_ROUTE_FORMAT, only: [:create, :destroy] do resources :session, id: USERNAME_ROUTE_FORMAT, only: [:create, :destroy] do
collection do collection do
post "forgot_password" post "forgot_password"
@ -195,18 +194,33 @@ Discourse::Application.routes.draw do
end end
resources :user_actions resources :user_actions
# We've renamed popular to latest. If people access it we want a permanent redirect.
get "popular" => "list#popular_redirect"
get "popular/more" => "list#popular_redirect"
resources :categories, :except => :show resources :categories, :except => :show
get "category/:id/show" => "categories#show" get "category/:id/show" => "categories#show"
post "category/:category_id/move" => "categories#move", as: "category_move" post "category/:category_id/move" => "categories#move", as: "category_move"
get "category/:category.rss" => "list#category_feed", format: :rss, as: "category_feed" get "category/:category.rss" => "list#category_feed", format: :rss, as: "category_feed"
get "category/:category" => "list#category", as: "category_list" get "category/:category" => "list#category"
get "category/:category/none" => "list#category_none", as: "category_list_none" get "category/:category/more" => "list#category"
get "category/:category/more" => "list#category", as: "category_list_more" get "category/:category/none" => "list#category_none"
get "category/:category/none/more" => "list#category_none"
get "category/:parent_category/:category" => "list#category"
get "category/:parent_category/:category/more" => "list#category"
# We"ve renamed popular to latest. If people access it we want a permanent redirect. get "top" => "list#top_lists"
get "popular" => "list#popular_redirect" get "category/:category/l/top" => "list#top_lists"
get "popular/more" => "list#popular_redirect" get "category/:parent_category/:category/l/top" => "list#top_lists"
TopTopic.periods.each do |period|
get "top/#{period}" => "list#top_#{period}"
get "top/#{period}/more" => "list#top_#{period}"
get "category/:category/l/top/#{period}" => "list#top_#{period}"
get "category/:category/l/top/#{period}/more" => "list#top_#{period}"
get "category/:parent_category/:category/l/top/#{period}" => "list#top_#{period}"
get "category/:parent_category/:category/l/top/#{period}/more" => "list#top_#{period}"
end
Discourse.anonymous_filters.each do |filter| Discourse.anonymous_filters.each do |filter|
get "#{filter}.rss" => "list##{filter}_feed", format: :rss get "#{filter}.rss" => "list##{filter}_feed", format: :rss
@ -215,26 +229,19 @@ Discourse::Application.routes.draw do
Discourse.filters.each do |filter| Discourse.filters.each do |filter|
get "#{filter}" => "list##{filter}" get "#{filter}" => "list##{filter}"
get "#{filter}/more" => "list##{filter}" get "#{filter}/more" => "list##{filter}"
get "category/:category/l/#{filter}" => "list##{filter}" get "category/:category/l/#{filter}" => "list##{filter}"
get "category/:category/l/#{filter}/more" => "list##{filter}" get "category/:category/l/#{filter}/more" => "list##{filter}"
get "category/:parent_category/:category/l/#{filter}" => "list##{filter}" get "category/:parent_category/:category/l/#{filter}" => "list##{filter}"
get "category/:parent_category/:category/l/#{filter}/more" => "list##{filter}" get "category/:parent_category/:category/l/#{filter}/more" => "list##{filter}"
end end
get "top" => "list#top"
get "category/:category/l/top" => "list#top"
get "category/:parent_category/:category/l/top" => "list#top"
get "category/:parent_category/:category" => "list#category", as: "category_list_parent"
get "search" => "search#query" get "search" => "search#query"
# Topics resource # Topics resource
get "t/:id" => "topics#show" get "t/:id" => "topics#show"
delete "t/:id" => "topics#destroy"
put "t/:id" => "topics#update"
post "t" => "topics#create" post "t" => "topics#create"
put "t/:id" => "topics#update"
delete "t/:id" => "topics#destroy"
post "topics/timings" post "topics/timings"
get "topics/similar_to" get "topics/similar_to"
get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: {username: USERNAME_ROUTE_FORMAT} get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: {username: USERNAME_ROUTE_FORMAT}
@ -277,7 +284,6 @@ Discourse::Application.routes.draw do
get "raw/:topic_id(/:post_number)" => "posts#markdown" get "raw/:topic_id(/:post_number)" => "posts#markdown"
resources :invites resources :invites
delete "invites" => "invites#destroy" delete "invites" => "invites#destroy"
@ -299,6 +305,6 @@ Discourse::Application.routes.draw do
# special case for categories # special case for categories
root to: "categories#index", constraints: HomePageConstraint.new("categories"), :as => "categories_index" root to: "categories#index", constraints: HomePageConstraint.new("categories"), :as => "categories_index"
# special case for top # special case for top
root to: "list#top", constraints: HomePageConstraint.new("top"), :as => "list_top" root to: "list#top_lists", constraints: HomePageConstraint.new("top"), :as => "top_lists"
end end

View File

@ -1,10 +1,13 @@
class CreateTopTopics < ActiveRecord::Migration class CreateTopTopics < ActiveRecord::Migration
PERIODS = [:yearly, :monthly, :weekly, :daily]
SORT_ORDERS = [:posts, :views, :likes]
def change def change
create_table :top_topics, force: true do |t| create_table :top_topics, force: true do |t|
t.belongs_to :topic t.belongs_to :topic
TopTopic.periods.each do |period| PERIODS.each do |period|
TopTopic.sort_orders.each do |sort| SORT_ORDERS.each do |sort|
t.integer "#{period}_#{sort}_count".to_sym, null: false, default: 0 t.integer "#{period}_#{sort}_count".to_sym, null: false, default: 0
end end
end end
@ -13,8 +16,8 @@ class CreateTopTopics < ActiveRecord::Migration
add_index :top_topics, :topic_id, unique: true add_index :top_topics, :topic_id, unique: true
TopTopic.periods.each do |period| PERIODS.each do |period|
TopTopic.sort_orders.each do |sort| SORT_ORDERS.each do |sort|
add_index :top_topics, "#{period}_#{sort}_count".to_sym, order: 'desc' add_index :top_topics, "#{period}_#{sort}_count".to_sym, order: 'desc'
end end
end end

View File

@ -89,11 +89,17 @@ class TopicQuery
score = "#{period}_score" score = "#{period}_score"
create_list(:top, unordered: true) do |topics| create_list(:top, unordered: true) do |topics|
topics.joins(:top_topic) topics.joins(:top_topic)
.where("top_topics.#{score} > 1") .where("top_topics.#{score} > 0")
.order("top_topics.#{score} DESC, topics.bumped_at DESC") .order("top_topics.#{score} DESC, topics.bumped_at DESC")
end end
end end
TopTopic.periods.each do |period|
define_method("list_top_#{period}") do
list_top_for(period)
end
end
def list_topics_by(user) def list_topics_by(user)
create_list(:user_topics) do |topics| create_list(:user_topics) do |topics|
topics.where(user_id: user.id) topics.where(user_id: user.id)

View File

@ -8,7 +8,7 @@ test("navigatedToHome", function() {
mock.expects("refresh").twice(); mock.expects("refresh").twice();
ok(Discourse.URL.navigatedToHome("/", "/")); ok(Discourse.URL.navigatedToHome("/", "/"));
var defaultFilter = "/" + Discourse.ListController.filters[0]; var defaultFilter = "/" + Discourse.ListController.FILTERS[0];
ok(Discourse.URL.navigatedToHome(defaultFilter, "/")); ok(Discourse.URL.navigatedToHome(defaultFilter, "/"));
ok(!Discourse.URL.navigatedToHome("/old", "/new")); ok(!Discourse.URL.navigatedToHome("/old", "/new"));