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() {
return (
Discourse.SiteSettings.login_required && !Discourse.User.current()
);
return Discourse.SiteSettings.login_required &&
!Discourse.User.current();
}.property(),
redirectIfLoginRequired: function(route) {

View File

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

View File

@ -30,7 +30,7 @@ Discourse.ListCategoriesController = Discourse.ObjectController.extend({
latestTopicOnly: function() {
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
updateTitle: function() {
if (this.get('filterMode') === 'categories') {
return Discourse.set('title', I18n.t('categories_list'));
Discourse.set('title', I18n.t('categories_list'));
} else {
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 {
return Discourse.set('title', I18n.t('topic.list'));
Discourse.set('title', I18n.t('topic.list'));
}
}
}.observes('filterMode', 'category'),
@ -134,5 +134,5 @@ Discourse.ListController = Discourse.Controller.extend({
});
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;
}.property(),
showThisYear: function() {
if (Discourse.User.current()) {
return !Discourse.User.currentProp("hasBeenSeenInTheLastMonth");
} else {
return true;
}
}.property()
hasDisplayedAllTopLists: Em.computed.and('content.yearly', 'content.monthly', 'content.weekly', 'content.daily'),
showMoreUrl: function(period) {
var url = "", category = this.get("category");
if (category) { url += category.get("url") + "/l"; }
url += "/top/" + period;
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({
pages: ['faq', 'tos', 'privacy', 'login'],
configs: {
PAGES: ['faq', 'tos', 'privacy', 'login'],
CONFIGS: {
'faq': 'faq_url',
'tos': 'tos_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
**/
navigatedToHome: function(oldPath, path) {
var defaultFilter = "/" + Discourse.ListController.filters[0];
var defaultFilter = "/" + Discourse.ListController.FILTERS[0];
if (path === "/" && (oldPath === "/" || oldPath === defaultFilter)) {
// Refresh our list

View File

@ -44,7 +44,7 @@ Discourse.Category = Discourse.Model.extend({
}.property('url'),
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'),
moreTopics: function() {
@ -54,7 +54,7 @@ Discourse.Category = Discourse.Model.extend({
save: function() {
var url = "/categories";
if (this.get('id')) {
url = "/categories/" + (this.get('id'));
url = "/categories/" + this.get('id');
}
return Discourse.ajax(url, {

View File

@ -16,7 +16,7 @@ Discourse.CategoryList = Ember.ArrayProxy.extend({
moveCategory: function(categoryId, position){
Discourse.ajax("/category/" + categoryId + "/move", {
type: 'POST',
data: {position: position}
data: { position: position }
});
}
});
@ -55,31 +55,20 @@ Discourse.CategoryList.reopenClass({
return categories;
},
list: function(filter) {
var self = this,
finder = null;
list: function() {
var self = this;
if (filter === 'categories') {
finder = PreloadStore.getAndRemove("categories_list", function() {
return Discourse.ajax("/categories.json");
});
} else {
finder = Discourse.ajax("/" + filter + ".json");
}
return finder.then(function(result) {
var categoryList = Discourse.TopicList.create();
categoryList.setProperties({
return PreloadStore.getAndRemove("categories_list", function() {
return Discourse.ajax("/categories.json");
}).then(function(result) {
return Discourse.CategoryList.create({
categories: self.categoriesFrom(result),
can_create_category: result.category_list.can_create_category,
can_create_topic: result.category_list.can_create_topic,
categories: self.categoriesFrom(result),
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
@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({
@ -69,26 +67,25 @@ Discourse.NavItem = Discourse.Model.extend({
});
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
fromText: function(text, opts) {
var countSummary = opts.countSummary,
split = text.split(","),
var split = text.split(","),
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 (!validNavNames.contains(testName)) return null;
if (!Discourse.NavItem.NAMES.contains(testName)) return null;
opts = {
return Discourse.NavItem.create({
name: name,
hasIcon: name === "unread" || name === "starred",
filters: split.splice(1),
category: opts.category
};
return Discourse.NavItem.create(opts);
category: opts.category,
});
}
});

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');
this.set('loaded', false);
var finder = finderFor(this.get('filter'), params);
finder().then(function (result) {
finderFor(this.get('filter'), params).then(function (result) {
var newTopics = Discourse.TopicList.topicsFrom(result),
topics = self.get('topics');

View File

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

View File

@ -14,34 +14,48 @@ Discourse.Route.buildRoutes(function() {
});
// Generate static page routes
Discourse.StaticController.pages.forEach(function(p) {
router.route(p, { path: "/" + p });
_.each(Discourse.StaticController.PAGES, function (page) {
router.route(page, { path: '/' + page });
});
// List routes
this.resource('list', { path: '/' }, function() {
this.resource("list", { path: "/" }, function() {
router = this;
// Generate routes for all our filters
Discourse.ListController.filters.forEach(function(filter) {
router.route(filter, { path: "/" + filter });
router.route(filter, { path: "/" + filter + "/more" });
router.route(filter + "Category", { path: "/category/:slug/l/" + filter });
router.route(filter + "Category", { path: "/category/:slug/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
this.route('categories');
// top
this.route('top');
this.route('topCategory', { path: '/category/:slug/l/top' });
this.route('topCategoryNone', { path: '/category/:slug/none/l/top' });
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
var homepage = Discourse.User.current() ?
Discourse.User.currentProp("homepage") :
Discourse.Utilities.defaultHomepage();
this.route(homepage, { path: '/' });
// filters
_.each(Discourse.ListController.FILTERS, function(filter) {
router.route(filter, { path: '/' + filter });
router.route(filter, { path: '/' + filter + '/more' });
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
this.route('categories', { path: '/categories' });
// category
// default filter for a category
this.route('category', { path: '/category/:slug' });
this.route('category', { path: '/category/:slug/more' });
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/more' });
// top page
this.route('top', { path: '/top' });
// homepage
var homepage = Discourse.User.current() ? Discourse.User.currentProp('homepage') : Discourse.Utilities.defaultHomepage();
this.route(homepage, { path: '/' });
});
// User routes
this.resource('user', { path: '/users/:username' }, function() {
this.route('index', { path: '/'} );
this.resource('userActivity', { path: '/activity' }, function() {
var self = this;
Object.keys(Discourse.UserAction.TYPES).forEach(function (userAction) {
self.route(userAction, { path: userAction.replace("_", "-") });
router = this;
_.map(Discourse.UserAction.TYPES, function (id, userAction) {
router.route(userAction, { path: userAction.replace('_', '-') });
});
});
this.resource('userPrivateMessages', { path: '/private-messages' }, function() {
this.route('mine', { path: '/mine' });
this.route('unread', { path: '/unread' });
this.route('mine');
this.route('unread');
});
this.resource('preferences', { path: '/preferences' }, function() {
this.route('username', { path: '/username' });
this.route('email', { path: '/email' });
this.resource('preferences', function() {
this.route('username');
this.route('email');
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);
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); },
deactivate: function() {
this._super();
this.controllerFor('list').setProperties({
canCreateTopic: false,
filterMode: ''
});
},
renderTemplate: function() {
this.render('listTopics', {
into: 'list',
outlet: 'listView',
controller: 'listTopics'
});
this.render('listTopics', { into: 'list', outlet: 'listView', controller: 'listTopics' });
},
setupController: function() {
@ -44,7 +31,16 @@ Discourse.FilteredListRoute = Discourse.Route.extend({
listTopicsController.set('model', topicList);
Discourse.FilteredListRoute.scrollToLastPosition();
});
}
},
deactivate: function() {
this._super();
this.controllerFor('list').setProperties({
canCreateTopic: false,
filterMode: ''
});
},
});
Discourse.FilteredListRoute.reopenClass({
@ -57,6 +53,10 @@ Discourse.FilteredListRoute.reopenClass({
}
});
Discourse.ListController.filters.forEach(function(filter) {
Discourse["List" + (filter.capitalize()) + "Route"] = Discourse.FilteredListRoute.extend({ filter: filter });
_.each(Discourse.ListController.FILTERS, function(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({
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() {
var listTopicsController = this.controllerFor('listTopics');
if (listTopicsController) { listTopicsController.set('content', null); }
this.controllerFor('listTop').set('content', null);
this.controllerFor('listTopics').set('content', null);
return this.controllerFor('list').load('categories');
},
deactivate: function() {
activate: function() {
this._super();
this.controllerFor('list').set('canCreateCategory', false);
this.controllerFor('list').setProperties({ filterMode: 'categories', category: null });
},
renderTemplate: function() {
this.render(this.get('template'), { into: 'list', outlet: 'listView' });
},
redirect: function() { Discourse.redirectIfLoginRequired(this); },
afterModel: function(categoryList) {
this.controllerFor('list').setProperties({
@ -44,13 +28,23 @@ Discourse.ListCategoriesRoute = Discourse.Route.extend({
});
},
activate: function() {
this.controllerFor('list').setProperties({
filterMode: 'categories',
category: null
});
}
renderTemplate: function() {
this.render('listCategories', { into: 'list', outlet: 'listView' });
},
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
**/
Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
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) {
@ -37,33 +46,29 @@ Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
canCreateTopic: topicList.get('can_create_topic'),
category: category
});
self.controllerFor('listTopics').set('content', topicList);
self.controllerFor('listTopics').set('category', category);
self.controllerFor('listTopics').setProperties({
content: topicList,
category: category
});
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() {
this._super();
// Clear the search context
this.controllerFor('search').set('searchContext', null);
}
});
Discourse.ListCategoryNoneRoute = Discourse.ListCategoryRoute.extend({
noSubcategories: true
});
Discourse.ListCategoryNoneRoute = Discourse.ListCategoryRoute.extend({ 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() + "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({
model: function() {
return Discourse.TopList.find();
model: function(params) {
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() {
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() {
this.render('top', { into: 'list', outlet: 'listView' });
},
deactivate: function() {
this._super();
// Clear any filters when we leave the route
Discourse.URL.set('queryParams', null);
this.render('listTop', { into: 'list', outlet: 'listView' });
}
});
Discourse.ListTopCategoryRoute = Discourse.ListTopRoute.extend({});

View File

@ -6,16 +6,16 @@
@namespace 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() {
this.render('static');
},
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) {
Discourse.URL.redirectTo(Discourse.SiteSettings[config_key]);
} 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.UserPrivateMessagesUnreadRoute = createPMRoute('unread', 'private-messages-unread');
Discourse.UserActivityTopicsRoute = Discourse.UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.topics,
@ -45,6 +44,6 @@ Discourse.UserActivityStarredRoute = Discourse.UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.starred,
model: function() {
return Discourse.TopicList.find('starred', {user_id: this.modelFor('user').get('id') });
return Discourse.TopicList.find('starred', { user_id: this.modelFor('user').get('id') });
}
});
});

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>
{{/if}}
</td>
<td class="category">
{{categoryLink topic.category}}
</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'>

View File

@ -1,17 +1,14 @@
<div class='contents'>
<table id='topic-list' class='categories'>
<thead>
<tr>
<th class='category'>{{i18n categories.category}}</th>
<th class='latest'>{{i18n categories.latest}}</th>
<th class='stats topics'>{{i18n categories.topics}}</th>
<th class='stats posts'>{{i18n categories.posts}}
{{#if canEdit}}
<button title='{{i18n categories.toggle_ordering}}' class='btn toggle-admin no-text' {{action toggleOrdering}}><i class='fa fa-wrench'></i></button>
{{/if}}
</th>
</tr>
<tr>
<th class='category'>{{i18n categories.category}}</th>
<th class='latest'>{{i18n categories.latest}}</th>
<th class='stats topics'>{{i18n categories.topics}}</th>
<th class='stats posts'>{{i18n categories.posts}}
{{#if canEdit}}<button title='{{i18n categories.toggle_ordering}}' class='btn toggle-admin no-text' {{action toggleOrdering}}><i class='fa fa-wrench'></i></button>{{/if}}
</th>
</tr>
</thead>
<tbody>
{{#each model.categories}}
@ -19,9 +16,7 @@
<td class='category'>
<div>
<div class="pull-left">
{{#if controller.ordering}}
<i class="fa fa-bars"></i>
{{/if}}
{{#if controller.ordering}}<i class="fa fa-bars"></i>{{/if}}
{{categoryLink this allowUncategorized=true}}
{{#if unreadTopics}}
<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}}}
</div>
{{/if}}
{{#if subcategories}}
<div class='subcategories'>
{{i18n categories.subcategories}}
@ -102,10 +96,7 @@
</tr>
{{/each}}
</tbody>
</table>
</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";
}
return I18n.t("filters." + name + ".help", extra);
}.property("content.filter"),
}.property("content.name"),
name: function() {

View File

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

View File

@ -1,6 +1,15 @@
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]
skip_before_filter :check_xhr
@ -72,13 +81,15 @@ class ListController < ApplicationController
redirect_to latest_path, :status => 301
end
def top
def top_lists
discourse_expires_in 1.minute
top = generate_top_lists
respond_to do |format|
format.html do
@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'
end
format.json do
@ -87,6 +98,16 @@ class ListController < ApplicationController
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
def category_response(extra_opts=nil)
@ -181,19 +202,27 @@ class ListController < ApplicationController
def generate_top_lists
top = {}
topic_ids = Set.new
options = {
per_page: SiteSetting.topics_per_period_in_summary,
category: params[:category]
}
topic_query = TopicQuery.new(current_user, options)
periods = periods_since(current_user.try(:last_seen_at))
TopTopic.periods.each do |period|
options = {
per_page: SiteSetting.topics_per_period_in_summary,
except_topic_ids: topic_ids.to_a
}
list = TopicQuery.new(current_user, options).list_top_for(period)
topic_ids.merge(list.topic_ids)
top[period] = list
end
periods.each { |period| top[period] = topic_query.list_top_for(period) }
top
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

View File

@ -1,19 +1,12 @@
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|
attribute period
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

View File

@ -604,6 +604,7 @@ en:
latest: "There are no latest topics. That's sad."
hot: "There are no hot topics."
category: "There are no {{category}} topics."
top: "There are no top topics."
bottom:
latest: "There are no more latest topics."
hot: "There are no more hot topics."
@ -613,6 +614,7 @@ en:
unread: "There are no more unread topics."
starred: "There are no more starred topics."
category: "There are no more {{category}} topics."
top: "There are no more top topics."
rank_details:
toggle: toggle topic rank details
@ -1122,6 +1124,7 @@ en:
this_month: "This month"
this_week: "This week"
today: "Today"
other_periods: "dig into other periods"
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."
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"
post "email/resubscribe/:key" => "email#resubscribe", as: "email_resubscribe"
resources :session, id: USERNAME_ROUTE_FORMAT, only: [:create, :destroy] do
collection do
post "forgot_password"
@ -195,18 +194,33 @@ Discourse::Application.routes.draw do
end
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
get "category/:id/show" => "categories#show"
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" => "list#category", as: "category_list"
get "category/:category/none" => "list#category_none", as: "category_list_none"
get "category/:category/more" => "list#category", as: "category_list_more"
get "category/:category" => "list#category"
get "category/:category/more" => "list#category"
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 "popular" => "list#popular_redirect"
get "popular/more" => "list#popular_redirect"
get "top" => "list#top_lists"
get "category/:category/l/top" => "list#top_lists"
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|
get "#{filter}.rss" => "list##{filter}_feed", format: :rss
@ -215,26 +229,19 @@ Discourse::Application.routes.draw do
Discourse.filters.each do |filter|
get "#{filter}" => "list##{filter}"
get "#{filter}/more" => "list##{filter}"
get "category/:category/l/#{filter}" => "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}/more" => "list##{filter}"
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"
# Topics resource
get "t/:id" => "topics#show"
delete "t/:id" => "topics#destroy"
put "t/:id" => "topics#update"
post "t" => "topics#create"
put "t/:id" => "topics#update"
delete "t/:id" => "topics#destroy"
post "topics/timings"
get "topics/similar_to"
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"
resources :invites
delete "invites" => "invites#destroy"
@ -299,6 +305,6 @@ Discourse::Application.routes.draw do
# special case for categories
root to: "categories#index", constraints: HomePageConstraint.new("categories"), :as => "categories_index"
# 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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ test("navigatedToHome", function() {
mock.expects("refresh").twice();
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("/old", "/new"));