From 567d2bd23cef5ef4c3c9fd1ded897c97fba3edf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 24 Dec 2013 00:50:36 +0100 Subject: [PATCH] add top page --- Gemfile | 1 - Gemfile_rails4.lock | 7 - .../routes/admin_site_content_edit_route.js | 2 +- ...t_controller.js => list_controller.js.erb} | 15 +- .../models/{nav_item.js => nav_item.js.erb} | 4 +- .../javascripts/discourse/models/top_list.js | 34 ++ .../discourse/models/topic_list.js | 41 ++- .../discourse/models/topic_tracking_state.js | 4 +- .../discourse/routes/application_routes.js | 11 +- .../discourse/routes/filtered_list_route.js | 9 +- .../discourse/routes/list_category_route.js | 4 +- .../discourse/routes/list_top_route.js | 24 ++ .../discourse/routes/preferences_routes.js | 6 +- .../components/basic-topic-list.js.handlebars | 2 +- .../{quote.js.shbrs => quote.js.handlebars} | 0 .../discourse/templates/top.js.handlebars | 9 + .../discourse/views/list/list_topics_view.js | 3 - .../views/list/topic_list_item_view.js | 2 +- .../stylesheets/desktop/topic-list.scss | 3 + app/controllers/list_controller.rb | 51 ++- app/jobs/scheduled/periodical_updates.rb | 4 +- app/models/category_featured_topic.rb | 2 +- app/models/site_setting.rb | 2 +- app/models/top_topic.rb | 81 +++++ app/models/topic.rb | 1 + app/models/topic_list.rb | 1 - app/serializers/top_list_serializer.rb | 19 ++ app/views/list/list.erb | 2 +- app/views/list/top.erb | 1 + config/locales/client.en.yml | 9 +- config/locales/server.en.yml | 4 +- config/routes.rb | 319 +++++++++--------- config/site_settings.yml | 2 + .../20131223171005_create_top_topics.rb | 23 ++ ...ail.js.shbrs => quote_email.js.handlebars} | 0 lib/discourse.rb | 24 +- lib/oneboxer/discourse_local_onebox.rb | 2 +- lib/oneboxer/handlebars_onebox.rb | 2 +- ...hbrs => discourse_topic_onebox.handlebars} | 0 ....hbrs => discourse_user_onebox.handlebars} | 0 ...ist_onebox.hbrs => gist_onebox.handlebars} | 0 ...box.hbrs => github_blob_onebox.handlebars} | 0 ...x.hbrs => github_commit_onebox.handlebars} | 0 ...s => github_pullrequest_onebox.handlebars} | 0 ...d_onebox.hbrs => oembed_onebox.handlebars} | 0 ....hbrs => rottentomatoes_onebox.handlebars} | 0 ...e_onebox.hbrs => simple_onebox.handlebars} | 0 ....hbrs => stack_exchange_onebox.handlebars} | 0 ..._onebox.hbrs => twitter_onebox.handlebars} | 0 lib/pretty_text.rb | 4 +- lib/topic_query.rb | 10 +- lib/topic_query_sql.rb | 3 +- spec/controllers/list_controller_spec.rb | 14 +- spec/models/top_topic_spec.rb | 30 ++ .../lib/simple_handlebars_rails.rb | 10 - .../simple_handlebars_template.rb | 38 --- .../simple_handlebars_rails.gemspec | 17 - 57 files changed, 532 insertions(+), 324 deletions(-) rename app/assets/javascripts/discourse/controllers/{list_controller.js => list_controller.js.erb} (89%) rename app/assets/javascripts/discourse/models/{nav_item.js => nav_item.js.erb} (93%) create mode 100644 app/assets/javascripts/discourse/models/top_list.js create mode 100644 app/assets/javascripts/discourse/routes/list_top_route.js rename app/assets/javascripts/discourse/templates/{quote.js.shbrs => quote.js.handlebars} (100%) create mode 100644 app/assets/javascripts/discourse/templates/top.js.handlebars create mode 100644 app/models/top_topic.rb create mode 100644 app/serializers/top_list_serializer.rb create mode 100644 app/views/list/top.erb create mode 100644 db/migrate/20131223171005_create_top_topics.rb rename lib/assets/{quote_email.js.shbrs => quote_email.js.handlebars} (100%) rename lib/oneboxer/templates/{discourse_topic_onebox.hbrs => discourse_topic_onebox.handlebars} (100%) rename lib/oneboxer/templates/{discourse_user_onebox.hbrs => discourse_user_onebox.handlebars} (100%) rename lib/oneboxer/templates/{gist_onebox.hbrs => gist_onebox.handlebars} (100%) rename lib/oneboxer/templates/{github_blob_onebox.hbrs => github_blob_onebox.handlebars} (100%) rename lib/oneboxer/templates/{github_commit_onebox.hbrs => github_commit_onebox.handlebars} (100%) rename lib/oneboxer/templates/{github_pullrequest_onebox.hbrs => github_pullrequest_onebox.handlebars} (100%) rename lib/oneboxer/templates/{oembed_onebox.hbrs => oembed_onebox.handlebars} (100%) rename lib/oneboxer/templates/{rottentomatoes_onebox.hbrs => rottentomatoes_onebox.handlebars} (100%) rename lib/oneboxer/templates/{simple_onebox.hbrs => simple_onebox.handlebars} (100%) rename lib/oneboxer/templates/{stack_exchange_onebox.hbrs => stack_exchange_onebox.handlebars} (100%) rename lib/oneboxer/templates/{twitter_onebox.hbrs => twitter_onebox.handlebars} (100%) create mode 100644 spec/models/top_topic_spec.rb delete mode 100644 vendor/gems/simple_handlebars_rails/lib/simple_handlebars_rails.rb delete mode 100644 vendor/gems/simple_handlebars_rails/lib/simple_handlebars_rails/simple_handlebars_template.rb delete mode 100644 vendor/gems/simple_handlebars_rails/simple_handlebars_rails.gemspec diff --git a/Gemfile b/Gemfile index 02b0041f820..39834da9b01 100644 --- a/Gemfile +++ b/Gemfile @@ -71,7 +71,6 @@ gem 'barber' gem 'message_bus' gem 'rails_multisite', path: 'vendor/gems/rails_multisite' -gem 'simple_handlebars_rails', path: 'vendor/gems/simple_handlebars_rails' gem 'redcarpet', require: false gem 'airbrake', '3.1.2', require: false # errbit is broken with 3.1.3 for now diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock index fa39ee6ec57..99e514c4f74 100644 --- a/Gemfile_rails4.lock +++ b/Gemfile_rails4.lock @@ -8,12 +8,6 @@ PATH specs: rails_multisite (0.0.1) -PATH - remote: vendor/gems/simple_handlebars_rails - specs: - simple_handlebars_rails (0.0.1) - rails (> 3.1) - GEM remote: https://rubygems.org/ specs: @@ -480,7 +474,6 @@ DEPENDENCIES sidekiq (= 2.15.1) sidekiq-failures sidetiq (>= 0.3.6) - simple_handlebars_rails! simplecov sinatra slim diff --git a/app/assets/javascripts/admin/routes/admin_site_content_edit_route.js b/app/assets/javascripts/admin/routes/admin_site_content_edit_route.js index b654c6383de..553c7a7ee48 100644 --- a/app/assets/javascripts/admin/routes/admin_site_content_edit_route.js +++ b/app/assets/javascripts/admin/routes/admin_site_content_edit_route.js @@ -24,7 +24,7 @@ Discourse.AdminSiteContentEditRoute = Discourse.Route.extend({ this.render('admin/templates/site_content_edit', {into: 'admin/templates/site_contents'}); }, - exit: function() { + deactivate: function() { this._super(); this.render('admin/templates/site_contents_empty', {into: 'admin/templates/site_contents'}); }, diff --git a/app/assets/javascripts/discourse/controllers/list_controller.js b/app/assets/javascripts/discourse/controllers/list_controller.js.erb similarity index 89% rename from app/assets/javascripts/discourse/controllers/list_controller.js rename to app/assets/javascripts/discourse/controllers/list_controller.js.erb index 02d3158ec7c..76e7f404c0c 100644 --- a/app/assets/javascripts/discourse/controllers/list_controller.js +++ b/app/assets/javascripts/discourse/controllers/list_controller.js.erb @@ -25,19 +25,6 @@ Discourse.ListController = Discourse.Controller.extend({ }); }.property("category"), - /** - Refresh our current topic list - - @method refresh - **/ - refresh: function() { - var listTopicsController = this.get('controllers.listTopics'); - listTopicsController.set('model.loaded', false); - this.load(this.get('filterMode')).then(function (topicList) { - listTopicsController.set('model', topicList); - }); - }, - /** Load a list based on a filter @@ -135,5 +122,5 @@ Discourse.ListController = Discourse.Controller.extend({ }); Discourse.ListController.reopenClass({ - filters: ['latest', 'hot', 'favorited', 'read', 'unread', 'new', 'posted'] + filters: <%= Discourse.filters.map(&:to_s) %> }); diff --git a/app/assets/javascripts/discourse/models/nav_item.js b/app/assets/javascripts/discourse/models/nav_item.js.erb similarity index 93% rename from app/assets/javascripts/discourse/models/nav_item.js rename to app/assets/javascripts/discourse/models/nav_item.js.erb index 2c947886df0..60e3f20f0be 100644 --- a/app/assets/javascripts/discourse/models/nav_item.js +++ b/app/assets/javascripts/discourse/models/nav_item.js.erb @@ -6,8 +6,8 @@ @namespace Discourse @module Discourse **/ -var validNavNames = ['latest', 'hot', 'categories', 'category', 'favorited', 'unread', 'new', 'read', 'posted']; -var validAnon = ['latest', 'hot', 'categories', 'category']; +var validNavNames = <%= Discourse.top_menu_items.map(&:to_s) %>; +var validAnon = <%= Discourse.anonymous_top_menu_items.map(&:to_s) %>; Discourse.NavItem = Discourse.Model.extend({ diff --git a/app/assets/javascripts/discourse/models/top_list.js b/app/assets/javascripts/discourse/models/top_list.js new file mode 100644 index 00000000000..bb67154b6c6 --- /dev/null +++ b/app/assets/javascripts/discourse/models/top_list.js @@ -0,0 +1,34 @@ +/** + 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; + }); + } +}); diff --git a/app/assets/javascripts/discourse/models/topic_list.js b/app/assets/javascripts/discourse/models/topic_list.js index 7fcbe5f0437..71aa5b7831c 100644 --- a/app/assets/javascripts/discourse/models/topic_list.js +++ b/app/assets/javascripts/discourse/models/topic_list.js @@ -181,6 +181,28 @@ Discourse.TopicList.reopenClass({ }); }, + from: function(result, filter, params) { + var topicList = Discourse.TopicList.create({ + inserted: Em.A(), + filter: filter, + params: params || {}, + topics: Discourse.TopicList.topicsFrom(result), + can_create_topic: result.topic_list.can_create_topic, + more_topics_url: result.topic_list.more_topics_url, + draft_key: result.topic_list.draft_key, + draft_sequence: result.topic_list.draft_sequence, + draft: result.topic_list.draft, + canViewRankDetails: result.topic_list.can_view_rank_details, + loaded: true + }); + + if (result.topic_list.filtered_category) { + topicList.set('category', Discourse.Category.create(result.topic_list.filtered_category)); + } + + return topicList; + }, + /** Lists topics on a given menu item @@ -206,24 +228,7 @@ Discourse.TopicList.reopenClass({ find: function(filter, params) { return PreloadStore.getAndRemove("topic_list", finderFor(filter, params)).then(function(result) { - var topicList = Discourse.TopicList.create({ - inserted: Em.A(), - filter: filter, - params: params || {}, - topics: Discourse.TopicList.topicsFrom(result), - can_create_topic: result.topic_list.can_create_topic, - more_topics_url: result.topic_list.more_topics_url, - draft_key: result.topic_list.draft_key, - draft_sequence: result.topic_list.draft_sequence, - draft: result.topic_list.draft, - canViewRankDetails: result.topic_list.can_view_rank_details, - loaded: true - }); - - if (result.topic_list.filtered_category) { - topicList.set('category', Discourse.Category.create(result.topic_list.filtered_category)); - } - return topicList; + return Discourse.TopicList.from(result, filter, params); }); } diff --git a/app/assets/javascripts/discourse/models/topic_tracking_state.js b/app/assets/javascripts/discourse/models/topic_tracking_state.js index c19de09a764..20b82cf6e0a 100644 --- a/app/assets/javascripts/discourse/models/topic_tracking_state.js +++ b/app/assets/javascripts/discourse/models/topic_tracking_state.js @@ -167,9 +167,9 @@ Discourse.TopicTrackingState = Discourse.Model.extend({ lookupCount: function(name, category){ var categoryName = Em.get(category, "name"); - if(name==="new") { + if(name === "new") { return this.countNew(categoryName); - } else if(name==="unread") { + } else if(name === "unread") { return this.countUnread(categoryName); } else { categoryName = name.split("/")[1]; diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js index 4305e9e37c3..6a66b30a54e 100644 --- a/app/assets/javascripts/discourse/routes/application_routes.js +++ b/app/assets/javascripts/discourse/routes/application_routes.js @@ -30,20 +30,25 @@ Discourse.Route.buildRoutes(function() { 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" }); - }); // the homepage is the first item of the 'top_menu' site setting var homepage = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0]; this.route(homepage, { path: '/' }); + // categories page this.route('categories', { path: '/categories' }); + + // category this.route('category', { path: '/category/:slug' }); this.route('category', { path: '/category/:slug/more' }); this.route('categoryNone', { path: '/category/:slug/none' }); this.route('categoryNone', { path: '/category/:slug/none/more' }); this.route('category', { path: '/category/:parentSlug/:slug' }); this.route('category', { path: '/category/:parentSlug/:slug/more' }); + + // top page + this.route('top', { path: '/top' }); }); // User routes @@ -58,8 +63,8 @@ Discourse.Route.buildRoutes(function() { }); this.resource('userPrivateMessages', { path: '/private-messages' }, function() { - this.route('mine', {path: '/mine'}); - this.route('unread', {path: '/unread'}); + this.route('mine', { path: '/mine' }); + this.route('unread', { path: '/unread' }); }); this.resource('preferences', { path: '/preferences' }, function() { diff --git a/app/assets/javascripts/discourse/routes/filtered_list_route.js b/app/assets/javascripts/discourse/routes/filtered_list_route.js index fd4cee253e1..408f9ea5009 100644 --- a/app/assets/javascripts/discourse/routes/filtered_list_route.js +++ b/app/assets/javascripts/discourse/routes/filtered_list_route.js @@ -10,12 +10,13 @@ Discourse.FilteredListRoute = Discourse.Route.extend({ redirect: function() { Discourse.redirectIfLoginRequired(this); }, - exit: function() { + deactivate: function() { this._super(); - var listController = this.controllerFor('list'); - listController.set('canCreateTopic', false); - listController.set('filterMode', ''); + this.controllerFor('list').setProperties({ + canCreateTopic: false, + filterMode: '' + }); }, renderTemplate: function() { diff --git a/app/assets/javascripts/discourse/routes/list_category_route.js b/app/assets/javascripts/discourse/routes/list_category_route.js index 725ac110d0d..944535cbdc1 100644 --- a/app/assets/javascripts/discourse/routes/list_category_route.js +++ b/app/assets/javascripts/discourse/routes/list_category_route.js @@ -63,9 +63,7 @@ Discourse.ListCategoryNoneRoute = Discourse.ListCategoryRoute.extend({ }); Discourse.ListController.filters.forEach(function(filter) { - Discourse["List" + (filter.capitalize()) + "CategoryRoute"] = Discourse.ListCategoryRoute.extend({ - filter: filter - }); + Discourse["List" + filter.capitalize() + "CategoryRoute"] = Discourse.ListCategoryRoute.extend({ filter: filter }); }); diff --git a/app/assets/javascripts/discourse/routes/list_top_route.js b/app/assets/javascripts/discourse/routes/list_top_route.js new file mode 100644 index 00000000000..672a3376915 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/list_top_route.js @@ -0,0 +1,24 @@ +Discourse.ListTopRoute = Discourse.Route.extend({ + + activate: function() { + // will mark the "top" navigation item as selected + this.controllerFor('list').setProperties({ + filterMode: 'top', + category: null + }); + }, + + model: function() { + return Discourse.TopList.find(); + }, + + renderTemplate: function() { + this.render('top', { into: 'list', outlet: 'listView' }); + }, + + deactivate: function() { + // Clear any filters when we leave the route + Discourse.URL.set('queryParams', null); + } + +}); diff --git a/app/assets/javascripts/discourse/routes/preferences_routes.js b/app/assets/javascripts/discourse/routes/preferences_routes.js index ac5c0451764..1b2209931b8 100644 --- a/app/assets/javascripts/discourse/routes/preferences_routes.js +++ b/app/assets/javascripts/discourse/routes/preferences_routes.js @@ -73,7 +73,7 @@ Discourse.PreferencesAboutRoute = Discourse.RestrictedUserRoute.extend({ }, // A bit odd, but if we leave to /preferences we need to re-render that outlet - exit: function() { + deactivate: function() { this._super(); this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' }); }, @@ -119,7 +119,7 @@ Discourse.PreferencesEmailRoute = Discourse.RestrictedUserRoute.extend({ }, // A bit odd, but if we leave to /preferences we need to re-render that outlet - exit: function() { + deactivate: function() { this._super(); this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' }); } @@ -143,7 +143,7 @@ Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({ }, // A bit odd, but if we leave to /preferences we need to re-render that outlet - exit: function() { + deactivate: function() { this._super(); this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' }); }, diff --git a/app/assets/javascripts/discourse/templates/components/basic-topic-list.js.handlebars b/app/assets/javascripts/discourse/templates/components/basic-topic-list.js.handlebars index 83c5ee7c8ca..a3f35d95c22 100644 --- a/app/assets/javascripts/discourse/templates/components/basic-topic-list.js.handlebars +++ b/app/assets/javascripts/discourse/templates/components/basic-topic-list.js.handlebars @@ -73,4 +73,4 @@ {{/if}} {{else}}
{{i18n loading}}
-{{/if}} \ No newline at end of file +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/quote.js.shbrs b/app/assets/javascripts/discourse/templates/quote.js.handlebars similarity index 100% rename from app/assets/javascripts/discourse/templates/quote.js.shbrs rename to app/assets/javascripts/discourse/templates/quote.js.handlebars diff --git a/app/assets/javascripts/discourse/templates/top.js.handlebars b/app/assets/javascripts/discourse/templates/top.js.handlebars new file mode 100644 index 00000000000..8bea38b4e1d --- /dev/null +++ b/app/assets/javascripts/discourse/templates/top.js.handlebars @@ -0,0 +1,9 @@ +

{{i18n filters.top.this_year}}

+{{basic-topic-list topicList=content.yearly}} +

{{i18n filters.top.this_month}}

+{{basic-topic-list topicList=content.monthly}} +

{{i18n filters.top.this_week}}

+{{basic-topic-list topicList=content.weekly}} +

{{i18n filters.top.today}}

+{{basic-topic-list topicList=content.daily}} +

{{#link-to "list.categories"}}{{i18n topic.browse_all_categories}}{{/link-to}} {{i18n or}} {{#link-to 'list.latest'}}{{i18n topic.view_latest_topics}}{{/link-to}}

diff --git a/app/assets/javascripts/discourse/views/list/list_topics_view.js b/app/assets/javascripts/discourse/views/list/list_topics_view.js index 1dd65f62db7..74602e49d94 100644 --- a/app/assets/javascripts/discourse/views/list/list_topics_view.js +++ b/app/assets/javascripts/discourse/views/list/list_topics_view.js @@ -60,7 +60,4 @@ Discourse.ListTopicsView = Discourse.View.extend(Discourse.LoadMore, { this.saveScrollPosition(); } - }); - - diff --git a/app/assets/javascripts/discourse/views/list/topic_list_item_view.js b/app/assets/javascripts/discourse/views/list/topic_list_item_view.js index 393f662b942..fb91b9c2bb7 100644 --- a/app/assets/javascripts/discourse/views/list/topic_list_item_view.js +++ b/app/assets/javascripts/discourse/views/list/topic_list_item_view.js @@ -28,7 +28,7 @@ Discourse.TopicListItemView = Discourse.GroupedView.extend({ didInsertElement: function() { var session = Discourse.Session.current(); - // // highligth the last topic viewed + // highligth the last topic viewed if (session.get('lastTopicIdViewed') === this.get('content.id')) { session.set('lastTopicIdViewed', null); this.highlight(); diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 1bdffb9b023..fc9276125a1 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -454,6 +454,9 @@ #list-area { margin-bottom: 300px; + h2 { + margin: 20px 0 10px; + } .topic-statuses .topic-status i {font-size: 15px;} diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 18b0d7ddad7..a9e1b03171a 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -1,26 +1,25 @@ class ListController < ApplicationController - before_filter :ensure_logged_in, except: [:latest, :hot, :category, :category_feed, :latest_feed, :hot_feed, :topics_by] + before_filter :ensure_logged_in, except: [:latest, :hot, :category, :top, :category_feed, :latest_feed, :hot_feed, :topics_by] before_filter :set_category, only: [:category, :category_feed] skip_before_filter :check_xhr # Create our filters - [:latest, :hot, :favorited, :read, :posted, :unread, :new].each do |filter| + Discourse.filters.each do |filter| define_method(filter) do list_opts = build_topic_list_options user = list_target_user list = TopicQuery.new(user, list_opts).public_send("list_#{filter}") list.more_topics_url = construct_url_with(filter, list_opts) - if [:latest, :hot].include?(filter) + if Discourse.anonymous_filters.include?(filter) @description = SiteSetting.site_description @rss = filter end - respond(list) end end - [:latest, :hot].each do |filter| + Discourse.anonymous_filters.each do |filter| define_method("#{filter}_feed") do discourse_expires_in 1.minute @@ -29,6 +28,7 @@ class ListController < ApplicationController @description = I18n.t("rss_description.#{filter}") @atom_link = "#{Discourse.base_url}/#{filter}.rss" @topic_list = TopicQuery.new.public_send("list_#{filter}") + render 'list', formats: [:rss] end end @@ -72,6 +72,22 @@ class ListController < ApplicationController redirect_to latest_path, :status => 301 end + def top + sort_order = params[:sort_order] || "posts" + top = generate_top_lists_by(sort_order) + + respond_to do |format| + format.html do + @top = top + store_preloaded('top_list', MultiJson.dump(TopListSerializer.new(top, scope: guardian, root: false))) + render 'top' + end + format.json do + render json: MultiJson.dump(TopListSerializer.new(top, scope: guardian, root: false)) + end + end + end + protected def category_response(extra_opts=nil) @@ -84,15 +100,12 @@ class ListController < ApplicationController end def respond(list) + discourse_expires_in 1.minute + list.draft = Draft.get(current_user, list.draft_key, list.draft_sequence) if current_user list.draft_key = Draft::NEW_TOPIC list.draft_sequence = DraftSequence.current(current_user, Draft::NEW_TOPIC) - draft = Draft.get(current_user, list.draft_key, list.draft_sequence) if current_user - list.draft = draft - - discourse_expires_in 1.minute - respond_to do |format| format.html do @list = list @@ -165,4 +178,22 @@ class ListController < ApplicationController method = url_prefix.blank? ? "#{action}_path" : "#{url_prefix}_#{action}_path" public_send(method, opts.merge(next_page_params(opts))) end + + def generate_top_lists_by(sort_order) + top = {} + topic_ids = Set.new + + 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(sort_order, period) + topic_ids.merge(list.topic_ids) + top[period] = list + end + + top + end + end diff --git a/app/jobs/scheduled/periodical_updates.rb b/app/jobs/scheduled/periodical_updates.rb index aaabcf2888b..561cda17317 100644 --- a/app/jobs/scheduled/periodical_updates.rb +++ b/app/jobs/scheduled/periodical_updates.rb @@ -8,7 +8,6 @@ module Jobs recurrence { hourly.minute_of_hour(3, 18, 33, 48) } def execute(args) - # Update the average times Post.calculate_avg_time Topic.calculate_avg_time @@ -25,6 +24,9 @@ module Jobs # Refresh Hot Topics HotTopic.refresh! + # Refresh Top Topics + TopTopic.refresh! + # Automatically close stuff that we missed Topic.auto_close end diff --git a/app/models/category_featured_topic.rb b/app/models/category_featured_topic.rb index e5f516c969d..9540edf4de9 100644 --- a/app/models/category_featured_topic.rb +++ b/app/models/category_featured_topic.rb @@ -15,7 +15,7 @@ class CategoryFeaturedTopic < ActiveRecord::Base def self.feature_topics_for(c) return if c.blank? - query = TopicQuery.new(self.fake_admin, per_page: SiteSetting.category_featured_topics, except_topic_id: c.topic_id, visible: true) + query = TopicQuery.new(self.fake_admin, per_page: SiteSetting.category_featured_topics, except_topic_ids: [c.topic_id], visible: true) results = query.list_category(c).topic_ids.to_a CategoryFeaturedTopic.transaction do diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index e670d5da725..c04b7a28e14 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -53,7 +53,7 @@ class SiteSetting < ActiveRecord::Base end def self.anonymous_menu_items - @anonymous_menu_items ||= Set.new ['latest', 'hot', 'categories', 'category'] + @anonymous_menu_items ||= Set.new Discourse.anonymous_filters.map(&:to_s) end def self.anonymous_homepage diff --git a/app/models/top_topic.rb b/app/models/top_topic.rb new file mode 100644 index 00000000000..e00ebeccb64 --- /dev/null +++ b/app/models/top_topic.rb @@ -0,0 +1,81 @@ +class TopTopic < ActiveRecord::Base + + belongs_to :topic + + def self.periods + @periods ||= %i{yearly monthly weekly daily} + end + + def self.sort_orders + @sort_orders ||= %i{posts views likes} + end + + def self.refresh! + transaction do + # clean up the table + exec_sql("DELETE FROM top_topics") + # insert the list of all the visible topics + exec_sql("INSERT INTO top_topics (topic_id) + SELECT id + FROM topics + WHERE deleted_at IS NULL + AND visible + AND NOT archived") + # update all the counter caches + TopTopic.periods.each do |period| + TopTopic.sort_orders.each do |sort| + TopTopic.send("update_#{sort}_count_for", period) + end + end + end + end + + def self.update_posts_count_for(period) + sql = "SELECT topic_id, COUNT(*) AS count + FROM posts p + WHERE p.created_at >= :from + AND p.deleted_at IS NULL + AND NOT p.hidden + GROUP BY topic_id" + + TopTopic.update_top_topics(period, "posts", sql) + end + + def self.update_views_count_for(period) + sql = "SELECT parent_id as topic_id, COUNT(*) AS count + FROM views v + WHERE v.viewed_at >= :from + GROUP BY topic_id" + + TopTopic.update_top_topics(period, "views", sql) + end + + def self.update_likes_count_for(period) + sql = "SELECT topic_id, SUM(like_count) AS count + FROM posts p + WHERE p.created_at >= :from + AND p.deleted_at IS NULL + AND NOT p.hidden + GROUP BY topic_id" + + TopTopic.update_top_topics(period, "likes", sql) + end + + def self.start_of(period) + case period + when :yearly then 1.year.ago + when :monthly then 1.month.ago + when :weekly then 1.week.ago + when :daily then 1.day.ago + end + end + + def self.update_top_topics(period, sort, inner_join) + exec_sql("UPDATE top_topics + SET #{period}_#{sort}_count = c.count + FROM top_topics tt + INNER JOIN (#{inner_join}) c ON tt.topic_id = c.topic_id + WHERE tt.topic_id = top_topics.topic_id", from: start_of(period)) + end + +end diff --git a/app/models/topic.rb b/app/models/topic.rb index 7b6a4bbdee5..dc838e5574e 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -82,6 +82,7 @@ class Topic < ActiveRecord::Base has_many :allowed_users, through: :topic_allowed_users, source: :user has_one :hot_topic + has_one :top_topic belongs_to :user belongs_to :last_poster, class_name: 'User', foreign_key: :last_post_user_id belongs_to :featured_user1, class_name: 'User', foreign_key: :featured_user1_id diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index 0d8ed9ca085..c57a3490bf6 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -51,7 +51,6 @@ class TopicList end def has_rank_details? - # Only moderators can see rank details return false unless @current_user && @current_user.staff? diff --git a/app/serializers/top_list_serializer.rb b/app/serializers/top_list_serializer.rb new file mode 100644 index 00000000000..753830f33bd --- /dev/null +++ b/app/serializers/top_list_serializer.rb @@ -0,0 +1,19 @@ +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| + define_method(period) do + TopicListSerializer.new(object[period], scope: scope).as_json + end + end + +end diff --git a/app/views/list/list.erb b/app/views/list/list.erb index cb0e42e95c6..d25d8a43c5f 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -1,6 +1,6 @@
<% @list.topics.each do |t| %> -<%= t.title %> '>(<%= t.posts_count %>)
+ <%= t.title %> '>(<%= t.posts_count %>)
<% end %>
diff --git a/app/views/list/top.erb b/app/views/list/top.erb new file mode 100644 index 00000000000..7c3de69f12d --- /dev/null +++ b/app/views/list/top.erb @@ -0,0 +1 @@ +

<%= t 'powered_by_html' %>

diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f50d4b0b30a..183430eadd2 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -668,7 +668,7 @@ en: browse_all_categories: Browse all categories - view_latest_topics: view latest topics + view_latest_topics: view latest topics. suggest_create_topic: Why not create a topic? read_position_reset: "Your read position has been reset." jump_reply_up: jump to earlier reply @@ -1102,6 +1102,13 @@ en: one: "{{categoryName}} (1)" other: "{{categoryName}} ({{count}})" help: "latest topics in the {{categoryName}} category" + top: + title: "Top" + help: "a selection of the best topics" + this_year: "This year" + this_month: "This month" + this_week: "This week" + today: "Today" browser_update: 'Unfortunately, your browser is too old to work on this Discourse forum. Please upgrade your browser.' diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 732f48a8de8..28fe554220b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -442,7 +442,7 @@ en: github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' s3_config_warning: 'The server is configured to upload files to s3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key or s3_upload_bucket. Go to the Site Settings and update the settings. See "How to set up image uploads to S3?" to learn more.' image_magick_warning: 'The server is configured to create thumbnails of large images, but ImageMagick is not installed. Install ImageMagick using your favorite package manager or download the latest release.' - failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/discourse.conf file and ensure that the mail server settings are correct. See the failed jobs in Sidekiq.' + failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/discourse.conf file and ensure that the mail server settings are correct. See the failed jobs in Sidekiq.' default_logo_warning: "You haven't customized the logo images for your site. Update logo_url, logo_small_url, and favicon_url in the Site Settings." contact_email_missing: "You haven't provided a contact email for your site. Please update contact_email in the Site Settings." contact_email_invalid: "The site contact email is invalid. Please update contact_email in the Site Settings." @@ -586,6 +586,8 @@ en: suppress_reply_directly_below: "Don't show reply count on a post when there is a single reply directly below" suppress_reply_directly_above: "Don't show in-reply-to on a post when there is a single reply directly above" + topics_per_period_in_summary: "How many topics loaded by default on the top topics page" + allow_index_in_robots_txt: "Site should be indexed by search engines (update robots.txt)" email_domains_blacklist: "A pipe-delimited list of email domains that are not allowed. Example: mailinator.com|trashmail.net" email_domains_whitelist: "A pipe-delimited list of email domains that users may register with. WARNING: Users with email domains other than those listed will not be allowed." diff --git a/config/routes.rb b/config/routes.rb index 36fb3dbcf24..87573e59078 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,9 +1,9 @@ -require 'sidekiq/web' -require 'sidetiq/web' +require "sidekiq/web" +require "sidetiq/web" -require_dependency 'admin_constraint' -require_dependency 'staff_constraint' -require_dependency 'homepage_constraint' +require_dependency "admin_constraint" +require_dependency "staff_constraint" +require_dependency "homepage_constraint" # This used to be User#username_format, but that causes a preload of the User object # and makes Guard not work properly. @@ -13,165 +13,166 @@ Discourse::Application.routes.draw do match "/404", to: "exceptions#not_found", via: [:get, :post] - mount Sidekiq::Web => '/sidekiq', constraints: AdminConstraint.new + mount Sidekiq::Web => "/sidekiq", constraints: AdminConstraint.new resources :forums - get 'srv/status' => 'forums#status' + get "srv/status" => "forums#status" namespace :admin, constraints: StaffConstraint.new do - get '' => 'admin#index' + get "" => "admin#index" resources :site_settings, constraints: AdminConstraint.new do collection do - get 'category/:id' => 'site_settings#index' + get "category/:id" => "site_settings#index" end end - get 'reports/:type' => 'reports#show' + get "reports/:type" => "reports#show" resources :groups, constraints: AdminConstraint.new do collection do - post 'refresh_automatic_groups' => 'groups#refresh_automatic_groups' + post "refresh_automatic_groups" => "groups#refresh_automatic_groups" end - get 'users' + get "users" end resources :users, id: USERNAME_ROUTE_FORMAT do collection do - get 'list/:query' => 'users#index' - put 'approve-bulk' => 'users#approve_bulk' - delete 'reject-bulk' => 'users#reject_bulk' + get "list/:query" => "users#index" + put "approve-bulk" => "users#approve_bulk" + delete "reject-bulk" => "users#reject_bulk" end - put 'suspend' - put 'delete_all_posts' - put 'unsuspend' - put 'revoke_admin', constraints: AdminConstraint.new - put 'grant_admin', constraints: AdminConstraint.new - post 'generate_api_key', constraints: AdminConstraint.new - delete 'revoke_api_key', constraints: AdminConstraint.new - put 'revoke_moderation', constraints: AdminConstraint.new - put 'grant_moderation', constraints: AdminConstraint.new - put 'approve' - post 'refresh_browsers', constraints: AdminConstraint.new - put 'activate' - put 'deactivate' - put 'block' - put 'unblock' - put 'trust_level' + put "suspend" + put "delete_all_posts" + put "unsuspend" + put "revoke_admin", constraints: AdminConstraint.new + put "grant_admin", constraints: AdminConstraint.new + post "generate_api_key", constraints: AdminConstraint.new + delete "revoke_api_key", constraints: AdminConstraint.new + put "revoke_moderation", constraints: AdminConstraint.new + put "grant_moderation", constraints: AdminConstraint.new + put "approve" + post "refresh_browsers", constraints: AdminConstraint.new + put "activate" + put "deactivate" + put "block" + put "unblock" + put "trust_level" end resources :impersonate, constraints: AdminConstraint.new resources :email do collection do - post 'test' - get 'logs' - get 'preview-digest' => 'email#preview_digest' + post "test" + get "logs" + get "preview-digest" => "email#preview_digest" end end - scope '/logs' do + scope "/logs" do resources :staff_action_logs, only: [:index] resources :screened_emails, only: [:index] resources :screened_ip_addresses, only: [:index, :create, :update, :destroy] resources :screened_urls, only: [:index] end - get 'customize' => 'site_customizations#index', constraints: AdminConstraint.new - get 'flags' => 'flags#index' - get 'flags/:filter' => 'flags#index' - post 'flags/agree/:id' => 'flags#agree' - post 'flags/disagree/:id' => 'flags#disagree' - post 'flags/defer/:id' => 'flags#defer' + get "customize" => "site_customizations#index", constraints: AdminConstraint.new + get "flags" => "flags#index" + get "flags/:filter" => "flags#index" + post "flags/agree/:id" => "flags#agree" + post "flags/disagree/:id" => "flags#disagree" + post "flags/defer/:id" => "flags#defer" resources :site_customizations, constraints: AdminConstraint.new resources :site_contents, constraints: AdminConstraint.new resources :site_content_types, constraints: AdminConstraint.new resources :export, constraints: AdminConstraint.new - get 'version_check' => 'versions#show' + get "version_check" => "versions#show" resources :dashboard, only: [:index] do collection do - get 'problems' + get "problems" end end resources :api, only: [:index], constraints: AdminConstraint.new do collection do - post 'key' => 'api#create_master_key' - put 'key' => 'api#regenerate_key' - delete 'key' => 'api#revoke_key' + post "key" => "api#create_master_key" + put "key" => "api#regenerate_key" + delete "key" => "api#revoke_key" end end end # admin namespace - get 'email_preferences' => 'email#preferences_redirect', :as => 'email_preferences_redirect' - get 'email/unsubscribe/:key' => 'email#unsubscribe', as: 'email_unsubscribe' - post 'email/resubscribe/:key' => 'email#resubscribe', as: 'email_resubscribe' + get "email_preferences" => "email#preferences_redirect", :as => "email_preferences_redirect" + 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' + post "forgot_password" end end - get 'session/csrf' => 'session#csrf' - get 'composer-messages' => 'composer_messages#index' + get "session/csrf" => "session#csrf" + get "composer-messages" => "composer_messages#index" resources :users, except: [:show, :update] do collection do - get 'check_username' - get 'is_local_username' + get "check_username" + get "is_local_username" end end resources :static - post 'login' => 'static#enter' - get 'login' => 'static#show', id: 'login' - get 'faq' => 'static#show', id: 'faq' - get 'tos' => 'static#show', id: 'tos' - get 'privacy' => 'static#show', id: 'privacy' + post "login" => "static#enter" + get "login" => "static#show", id: "login" + get "faq" => "static#show", id: "faq" + get "tos" => "static#show", id: "tos" + get "privacy" => "static#show", id: "privacy" - get 'users/search/users' => 'users#search_users' - get 'users/password-reset/:token' => 'users#password_reset' - put 'users/password-reset/:token' => 'users#password_reset' - get 'users/activate-account/:token' => 'users#activate_account' - get 'users/authorize-email/:token' => 'users#authorize_email' - get 'users/hp' => 'users#get_honeypot_value' + get "users/search/users" => "users#search_users" + get "users/password-reset/:token" => "users#password_reset" + put "users/password-reset/:token" => "users#password_reset" + get "users/activate-account/:token" => "users#activate_account" + get "users/authorize-email/:token" => "users#authorize_email" + get "users/hp" => "users#get_honeypot_value" - get 'user_preferences' => 'users#user_preferences_redirect' - get 'users/:username/private-messages' => 'user_actions#private_messages', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'users/:username/private-messages/:filter' => 'user_actions#private_messages', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'users/:username' => 'users#show', as: 'userpage', constraints: {username: USERNAME_ROUTE_FORMAT} - put 'users/:username' => 'users#update', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'users/:username/preferences' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT}, as: :email_preferences - get 'users/:username/preferences/email' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT} - put 'users/:username/preferences/email' => 'users#change_email', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'users/:username/preferences/about-me' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'users/:username/preferences/username' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT} - put 'users/:username/preferences/username' => 'users#username', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'users/:username/avatar(/:size)' => 'users#avatar', constraints: {username: USERNAME_ROUTE_FORMAT} # LEGACY ROUTE - post 'users/:username/preferences/avatar' => 'users#upload_avatar', constraints: {username: USERNAME_ROUTE_FORMAT} - put 'users/:username/preferences/avatar/toggle' => 'users#toggle_avatar', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'users/:username/invited' => 'users#invited', constraints: {username: USERNAME_ROUTE_FORMAT} - post 'users/:username/send_activation_email' => 'users#send_activation_email', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'users/:username/activity' => 'users#show', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'users/:username/activity/:filter' => 'users#show', constraints: {username: USERNAME_ROUTE_FORMAT} + get "user_preferences" => "users#user_preferences_redirect" + get "users/:username/private-messages" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username/private-messages/:filter" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username" => "users#show", as: 'userpage', constraints: {username: USERNAME_ROUTE_FORMAT} + put "users/:username" => "users#update", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username/preferences" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT}, as: :email_preferences + get "users/:username/preferences/email" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} + put "users/:username/preferences/email" => "users#change_email", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username/preferences/about-me" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username/preferences/username" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} + put "users/:username/preferences/username" => "users#username", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username/avatar(/:size)" => "users#avatar", constraints: {username: USERNAME_ROUTE_FORMAT} # LEGACY ROUTE + post "users/:username/preferences/avatar" => "users#upload_avatar", constraints: {username: USERNAME_ROUTE_FORMAT} + put "users/:username/preferences/avatar/toggle" => "users#toggle_avatar", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username/invited" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT} + post "users/:username/send_activation_email" => "users#send_activation_email", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username/activity" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username/activity/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get 'uploads/:site/:id/:sha.:extension' => 'uploads#show', constraints: {site: /\w+/, id: /\d+/, sha: /[a-z0-9]{15,16}/i, extension: /\w{2,}/} - post 'uploads' => 'uploads#create' + get "uploads/:site/:id/:sha.:extension" => "uploads#show", constraints: {site: /\w+/, id: /\d+/, sha: /[a-z0-9]{15,16}/i, extension: /\w{2,}/} + post "uploads" => "uploads#create" + + get "posts/by_number/:topic_id/:post_number" => "posts#by_number" + get "posts/:id/reply-history" => "posts#reply_history" - get 'posts/by_number/:topic_id/:post_number' => 'posts#by_number' - get 'posts/:id/reply-history' => 'posts#reply_history' resources :posts do - put 'bookmark' - get 'replies' - get 'revisions/:revision' => 'posts#revisions' - put 'recover' + put "bookmark" + get "replies" + get "revisions/:revision" => "posts#revisions" + put "recover" collection do - delete 'destroy_many' + delete "destroy_many" end end - get 'p/:post_id/:user_id' => 'posts#short_link' + get "p/:post_id/:user_id" => "posts#short_link" resources :notifications @@ -180,38 +181,38 @@ Discourse::Application.routes.draw do resources :clicks do collection do - get 'track' + get "track" end end - get 'excerpt' => 'excerpt#show' + get "excerpt" => "excerpt#show" resources :post_actions do collection do - get 'users' - post 'clear_flags' + get "users" + post "clear_flags" end end resources :user_actions resources :categories, :except => :show - get 'category/:id/show' => 'categories#show' - post 'category/:category_id/move' => 'categories#move', as: 'category_move' + 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.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" - # 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' + # 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" - [:latest, :hot].each do |filter| + Discourse.anonymous_filters.each do |filter| get "#{filter}.rss" => "list##{filter}_feed", format: :rss end - [:latest, :hot, :favorited, :read, :posted, :unread, :new].each do |filter| + Discourse.filters.each do |filter| get "#{filter}" => "list##{filter}" get "#{filter}/more" => "list##{filter}" @@ -221,71 +222,75 @@ Discourse::Application.routes.draw do get "category/:parent_category/:category/l/#{filter}/more" => "list##{filter}" end - get 'category/:parent_category/:category' => 'list#category', as: 'category_list_parent' + get "top" => "list#top" + get "category/:category/l/top" => "list#top" + get "category/:parent_category/:category/l/top" => "list#top" - get 'search' => 'search#query' + 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' - post 'topics/timings' - get 'topics/similar_to' - get 'topics/created-by/:username' => 'list#topics_by', as: 'topics_by', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'topics/private-messages/:username' => 'list#private_messages', as: 'topics_private_messages', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'topics/private-messages-sent/:username' => 'list#private_messages_sent', as: 'topics_private_messages_sent', constraints: {username: USERNAME_ROUTE_FORMAT} - get 'topics/private-messages-unread/:username' => 'list#private_messages_unread', as: 'topics_private_messages_unread', constraints: {username: USERNAME_ROUTE_FORMAT} + get "t/:id" => "topics#show" + delete "t/:id" => "topics#destroy" + put "t/:id" => "topics#update" + post "t" => "topics#create" + post "topics/timings" + get "topics/similar_to" + get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: {username: USERNAME_ROUTE_FORMAT} + get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} + get "topics/private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", constraints: {username: USERNAME_ROUTE_FORMAT} + get "topics/private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", constraints: {username: USERNAME_ROUTE_FORMAT} # Topic routes - get 't/:slug/:topic_id/wordpress' => 'topics#wordpress', constraints: {topic_id: /\d+/} - get 't/:slug/:topic_id/moderator-liked' => 'topics#moderator_liked', constraints: {topic_id: /\d+/} - get 't/:topic_id/wordpress' => 'topics#wordpress', constraints: {topic_id: /\d+/} - get 't/:slug/:topic_id/summary' => 'topics#show', defaults: {summary: true}, constraints: {topic_id: /\d+/, post_number: /\d+/} - get 't/:topic_id/summary' => 'topics#show', constraints: {topic_id: /\d+/, post_number: /\d+/} - put 't/:slug/:topic_id' => 'topics#update', constraints: {topic_id: /\d+/} - put 't/:slug/:topic_id/star' => 'topics#star', constraints: {topic_id: /\d+/} - put 't/:topic_id/star' => 'topics#star', constraints: {topic_id: /\d+/} - put 't/:slug/:topic_id/status' => 'topics#status', constraints: {topic_id: /\d+/} - put 't/:topic_id/status' => 'topics#status', constraints: {topic_id: /\d+/} - put 't/:topic_id/clear-pin' => 'topics#clear_pin', constraints: {topic_id: /\d+/} - put 't/:topic_id/mute' => 'topics#mute', constraints: {topic_id: /\d+/} - put 't/:topic_id/unmute' => 'topics#unmute', constraints: {topic_id: /\d+/} - put 't/:topic_id/autoclose' => 'topics#autoclose', constraints: {topic_id: /\d+/} - put 't/:topic_id/remove-allowed-user' => 'topics#remove_allowed_user', constraints: {topic_id: /\d+/} - put 't/:topic_id/recover' => 'topics#recover', constraints: {topic_id: /\d+/} - get 't/:topic_id/:post_number' => 'topics#show', constraints: {topic_id: /\d+/, post_number: /\d+/} - get 't/:slug/:topic_id.rss' => 'topics#feed', format: :rss, constraints: {topic_id: /\d+/} - get 't/:slug/:topic_id' => 'topics#show', constraints: {topic_id: /\d+/} - get 't/:slug/:topic_id/:post_number' => 'topics#show', constraints: {topic_id: /\d+/, post_number: /\d+/} - get 't/:topic_id/posts' => 'topics#posts', constraints: {topic_id: /\d+/} - post 't/:topic_id/timings' => 'topics#timings', constraints: {topic_id: /\d+/} - post 't/:topic_id/invite' => 'topics#invite', constraints: {topic_id: /\d+/} - post 't/:topic_id/move-posts' => 'topics#move_posts', constraints: {topic_id: /\d+/} - post 't/:topic_id/merge-topic' => 'topics#merge_topic', constraints: {topic_id: /\d+/} - delete 't/:topic_id/timings' => 'topics#destroy_timings', constraints: {topic_id: /\d+/} + get "t/:slug/:topic_id/wordpress" => "topics#wordpress", constraints: {topic_id: /\d+/} + get "t/:slug/:topic_id/moderator-liked" => "topics#moderator_liked", constraints: {topic_id: /\d+/} + get "t/:topic_id/wordpress" => "topics#wordpress", constraints: {topic_id: /\d+/} + get "t/:slug/:topic_id/summary" => "topics#show", defaults: {summary: true}, constraints: {topic_id: /\d+/, post_number: /\d+/} + get "t/:topic_id/summary" => "topics#show", constraints: {topic_id: /\d+/, post_number: /\d+/} + put "t/:slug/:topic_id" => "topics#update", constraints: {topic_id: /\d+/} + put "t/:slug/:topic_id/star" => "topics#star", constraints: {topic_id: /\d+/} + put "t/:topic_id/star" => "topics#star", constraints: {topic_id: /\d+/} + put "t/:slug/:topic_id/status" => "topics#status", constraints: {topic_id: /\d+/} + put "t/:topic_id/status" => "topics#status", constraints: {topic_id: /\d+/} + put "t/:topic_id/clear-pin" => "topics#clear_pin", constraints: {topic_id: /\d+/} + put "t/:topic_id/mute" => "topics#mute", constraints: {topic_id: /\d+/} + put "t/:topic_id/unmute" => "topics#unmute", constraints: {topic_id: /\d+/} + put "t/:topic_id/autoclose" => "topics#autoclose", constraints: {topic_id: /\d+/} + put "t/:topic_id/remove-allowed-user" => "topics#remove_allowed_user", constraints: {topic_id: /\d+/} + put "t/:topic_id/recover" => "topics#recover", constraints: {topic_id: /\d+/} + get "t/:topic_id/:post_number" => "topics#show", constraints: {topic_id: /\d+/, post_number: /\d+/} + get "t/:slug/:topic_id.rss" => "topics#feed", format: :rss, constraints: {topic_id: /\d+/} + get "t/:slug/:topic_id" => "topics#show", constraints: {topic_id: /\d+/} + get "t/:slug/:topic_id/:post_number" => "topics#show", constraints: {topic_id: /\d+/, post_number: /\d+/} + get "t/:topic_id/posts" => "topics#posts", constraints: {topic_id: /\d+/} + post "t/:topic_id/timings" => "topics#timings", constraints: {topic_id: /\d+/} + post "t/:topic_id/invite" => "topics#invite", constraints: {topic_id: /\d+/} + post "t/:topic_id/move-posts" => "topics#move_posts", constraints: {topic_id: /\d+/} + post "t/:topic_id/merge-topic" => "topics#merge_topic", constraints: {topic_id: /\d+/} + delete "t/:topic_id/timings" => "topics#destroy_timings", constraints: {topic_id: /\d+/} - post 't/:topic_id/notifications' => 'topics#set_notifications' , constraints: {topic_id: /\d+/} + post "t/:topic_id/notifications" => "topics#set_notifications" , constraints: {topic_id: /\d+/} - get 'raw/:topic_id(/:post_number)' => 'posts#markdown' + get "raw/:topic_id(/:post_number)" => "posts#markdown" resources :invites - delete 'invites' => 'invites#destroy' + delete "invites" => "invites#destroy" - get 'onebox' => 'onebox#show' + get "onebox" => "onebox#show" - get 'error' => 'forums#error' + get "error" => "forums#error" - get 'message-bus/poll' => 'message_bus#poll' + get "message-bus/poll" => "message_bus#poll" - get 'draft' => 'draft#show' - post 'draft' => 'draft#update' - delete 'draft' => 'draft#destroy' + get "draft" => "draft#show" + post "draft" => "draft#update" + delete "draft" => "draft#destroy" - get 'robots.txt' => 'robots_txt#index' + get "robots.txt" => "robots_txt#index" - [:latest, :hot, :unread, :new, :favorited, :read, :posted].each do |filter| + Discourse.filters.each do |filter| root to: "list##{filter}", constraints: HomePageConstraint.new("#{filter}"), :as => "list_#{filter}" end # special case for categories diff --git a/config/site_settings.yml b/config/site_settings.yml index fe3c5a96f88..372d814f2e4 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -59,6 +59,8 @@ basic: relative_date_duration: client: true default: 30 + topics_per_period_in_summary: + default: 10 users: enable_local_logins: diff --git a/db/migrate/20131223171005_create_top_topics.rb b/db/migrate/20131223171005_create_top_topics.rb new file mode 100644 index 00000000000..1717b59bfc2 --- /dev/null +++ b/db/migrate/20131223171005_create_top_topics.rb @@ -0,0 +1,23 @@ +class CreateTopTopics < ActiveRecord::Migration + def change + create_table :top_topics do |t| + t.belongs_to :topic + + TopTopic.periods.each do |period| + TopTopic.sort_orders.each do |sort| + t.integer "#{period}_#{sort}_count".to_sym, null: false, default: 0 + end + end + + end + + add_index :top_topics, :topic_id, unique: true + + TopTopic.periods.each do |period| + TopTopic.sort_orders.each do |sort| + add_index :top_topics, "#{period}_#{sort}_count".to_sym, order: 'desc' + end + end + + end +end diff --git a/lib/assets/quote_email.js.shbrs b/lib/assets/quote_email.js.handlebars similarity index 100% rename from lib/assets/quote_email.js.shbrs rename to lib/assets/quote_email.js.handlebars diff --git a/lib/discourse.rb b/lib/discourse.rb index ac181bbb0e4..bd1622d3d4d 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -30,11 +30,29 @@ module Discourse # Cross site request forgery class CSRF < Exception; end + def self.filters + @filters ||= [:latest, :hot, :unread, :new, :favorited, :read, :posted] + end + + def self.anonymous_filters + @anonymous_filters ||= [:latest, :hot] + end + + def self.logged_in_filters + @logged_in_filters ||= Discourse.filters - Discourse.anonymous_filters + end + + def self.top_menu_items + @top_menu_items ||= Discourse.filters.concat([:category, :categories, :top]) + end + + def self.anonymous_top_menu_items + @anonymous_top_menu_items ||= Discourse.anonymous_filters.concat([:category, :categories, :top]) + end + def self.activate_plugins! @plugins = Plugin::Instance.find_all("#{Rails.root}/plugins") - @plugins.each do |plugin| - plugin.activate! - end + @plugins.each { |plugin| plugin.activate! } end def self.plugins diff --git a/lib/oneboxer/discourse_local_onebox.rb b/lib/oneboxer/discourse_local_onebox.rb index af76a481508..1c204062984 100644 --- a/lib/oneboxer/discourse_local_onebox.rb +++ b/lib/oneboxer/discourse_local_onebox.rb @@ -88,7 +88,7 @@ module Oneboxer end return nil unless @template - Mustache.render(File.read("#{Rails.root}/lib/oneboxer/templates/discourse_#{@template}_onebox.hbrs"), args) + Mustache.render(File.read("#{Rails.root}/lib/oneboxer/templates/discourse_#{@template}_onebox.handlebars"), args) rescue ActionController::RoutingError nil end diff --git a/lib/oneboxer/handlebars_onebox.rb b/lib/oneboxer/handlebars_onebox.rb index abe353e8bb2..5573431b838 100644 --- a/lib/oneboxer/handlebars_onebox.rb +++ b/lib/oneboxer/handlebars_onebox.rb @@ -10,7 +10,7 @@ module Oneboxer end def self.template_path(template_name) - "#{Rails.root}/lib/oneboxer/templates/#{template_name}.hbrs" + "#{Rails.root}/lib/oneboxer/templates/#{template_name}.handlebars" end def template_path(template_name) diff --git a/lib/oneboxer/templates/discourse_topic_onebox.hbrs b/lib/oneboxer/templates/discourse_topic_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/discourse_topic_onebox.hbrs rename to lib/oneboxer/templates/discourse_topic_onebox.handlebars diff --git a/lib/oneboxer/templates/discourse_user_onebox.hbrs b/lib/oneboxer/templates/discourse_user_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/discourse_user_onebox.hbrs rename to lib/oneboxer/templates/discourse_user_onebox.handlebars diff --git a/lib/oneboxer/templates/gist_onebox.hbrs b/lib/oneboxer/templates/gist_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/gist_onebox.hbrs rename to lib/oneboxer/templates/gist_onebox.handlebars diff --git a/lib/oneboxer/templates/github_blob_onebox.hbrs b/lib/oneboxer/templates/github_blob_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/github_blob_onebox.hbrs rename to lib/oneboxer/templates/github_blob_onebox.handlebars diff --git a/lib/oneboxer/templates/github_commit_onebox.hbrs b/lib/oneboxer/templates/github_commit_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/github_commit_onebox.hbrs rename to lib/oneboxer/templates/github_commit_onebox.handlebars diff --git a/lib/oneboxer/templates/github_pullrequest_onebox.hbrs b/lib/oneboxer/templates/github_pullrequest_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/github_pullrequest_onebox.hbrs rename to lib/oneboxer/templates/github_pullrequest_onebox.handlebars diff --git a/lib/oneboxer/templates/oembed_onebox.hbrs b/lib/oneboxer/templates/oembed_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/oembed_onebox.hbrs rename to lib/oneboxer/templates/oembed_onebox.handlebars diff --git a/lib/oneboxer/templates/rottentomatoes_onebox.hbrs b/lib/oneboxer/templates/rottentomatoes_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/rottentomatoes_onebox.hbrs rename to lib/oneboxer/templates/rottentomatoes_onebox.handlebars diff --git a/lib/oneboxer/templates/simple_onebox.hbrs b/lib/oneboxer/templates/simple_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/simple_onebox.hbrs rename to lib/oneboxer/templates/simple_onebox.handlebars diff --git a/lib/oneboxer/templates/stack_exchange_onebox.hbrs b/lib/oneboxer/templates/stack_exchange_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/stack_exchange_onebox.hbrs rename to lib/oneboxer/templates/stack_exchange_onebox.handlebars diff --git a/lib/oneboxer/templates/twitter_onebox.hbrs b/lib/oneboxer/templates/twitter_onebox.handlebars similarity index 100% rename from lib/oneboxer/templates/twitter_onebox.hbrs rename to lib/oneboxer/templates/twitter_onebox.handlebars diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 3a44e56d194..bdfb3001ea0 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -91,8 +91,8 @@ module PrettyText end end - ctx['quoteTemplate'] = File.open(app_root + 'app/assets/javascripts/discourse/templates/quote.js.shbrs') {|f| f.read} - ctx['quoteEmailTemplate'] = File.open(app_root + 'lib/assets/quote_email.js.shbrs') {|f| f.read} + ctx['quoteTemplate'] = File.open(app_root + 'app/assets/javascripts/discourse/templates/quote.js.handlebars') {|f| f.read} + ctx['quoteEmailTemplate'] = File.open(app_root + 'lib/assets/quote_email.js.handlebars') {|f| f.read} ctx.eval("HANDLEBARS_TEMPLATES = { 'quote': Handlebars.compile(quoteTemplate), 'quote_email': Handlebars.compile(quoteEmailTemplate), diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 5c08b25cbff..a1704d6461f 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -8,7 +8,7 @@ require_dependency 'topic_query_sql' class TopicQuery # Could be rewritten to %i if Ruby 1.9 is no longer supported - VALID_OPTIONS = %w(except_topic_id + VALID_OPTIONS = %w(except_topic_ids exclude_category limit page @@ -84,6 +84,12 @@ class TopicQuery create_list(:posted) {|l| l.where('tu.user_id IS NOT NULL') } end + def list_top(sort_order, period) + create_list(:top, unordered: true) do |topics| + topics.joins(:top_topic).order("top_topics.#{period}_#{sort_order}_count DESC, topics.bumped_at DESC") + end + end + def list_topics_by(user) create_list(:user_topics) do |topics| topics.where(user_id: user.id) @@ -227,7 +233,7 @@ class TopicQuery result = result.limit(options[:per_page]) unless options[:limit] == false result = result.visible if options[:visible] || @user.nil? || @user.regular? - result = result.where('topics.id <> ?', options[:except_topic_id]).references(:topics) if options[:except_topic_id] + result = result.where.not(topics: {id: options[:except_topic_ids]}).references(:topics) if options[:except_topic_ids] result = result.offset(options[:page].to_i * options[:per_page]) if options[:page] if options[:topic_ids] diff --git a/lib/topic_query_sql.rb b/lib/topic_query_sql.rb index d9595e2833b..8d0a58ba2c5 100644 --- a/lib/topic_query_sql.rb +++ b/lib/topic_query_sql.rb @@ -6,8 +6,7 @@ module TopicQuerySQL class << self # use the constants in conjuction with COALESCE to determine the order with regard to pinned - # topics that have been cleared by the user. There - # might be a cleaner way to do this. + # topics that have been cleared by the user. There might be a cleaner way to do this. def lowest_date "2010-01-01" end diff --git a/spec/controllers/list_controller_spec.rb b/spec/controllers/list_controller_spec.rb index 4408d3753f3..7b887b4c89f 100644 --- a/spec/controllers/list_controller_spec.rb +++ b/spec/controllers/list_controller_spec.rb @@ -13,14 +13,14 @@ describe ListController do describe 'indexes' do - [:latest, :hot].each do |filter| + Discourse.anonymous_filters.each do |filter| context "#{filter}" do before { xhr :get, filter } it { should respond_with(:success) } end end - [:favorited, :read, :posted, :unread, :new].each do |filter| + Discourse.logged_in_filters.each do |filter| context "#{filter}" do it { expect { xhr :get, filter }.to raise_error(Discourse::NotLoggedIn) } end @@ -39,7 +39,7 @@ describe ListController do describe 'RSS feeds' do - [:latest, :hot].each do |filter| + Discourse.anonymous_filters.each do |filter| it 'renders RSS' do get "#{filter}_feed", format: :rss @@ -175,14 +175,6 @@ describe ListController do end end - context 'hot' do - before do - xhr :get, :hot - end - - it { should respond_with(:success) } - end - context 'favorited' do it 'raises an error when not logged in' do lambda { xhr :get, :favorited }.should raise_error(Discourse::NotLoggedIn) diff --git a/spec/models/top_topic_spec.rb b/spec/models/top_topic_spec.rb new file mode 100644 index 00000000000..5640b900154 --- /dev/null +++ b/spec/models/top_topic_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe TopTopic do + + it { should belong_to :topic } + + context "refresh!" do + + let!(:t1) { Fabricate(:topic) } + let!(:t2) { Fabricate(:topic) } + + it "begins blank" do + TopTopic.all.should be_blank + end + + context "after calculating" do + + before do + TopTopic.refresh! + end + + it "should have top topics" do + TopTopic.pluck(:topic_id).should =~ [t1.id, t2.id] + end + + end + + end + +end diff --git a/vendor/gems/simple_handlebars_rails/lib/simple_handlebars_rails.rb b/vendor/gems/simple_handlebars_rails/lib/simple_handlebars_rails.rb deleted file mode 100644 index 784c9c5313d..00000000000 --- a/vendor/gems/simple_handlebars_rails/lib/simple_handlebars_rails.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'sprockets' -require 'sprockets/engines' -require 'simple_handlebars_rails/simple_handlebars_template' - -module SimpleHandlebarsRails - class Engine < Rails::Engine - end - - Sprockets.register_engine '.shbrs', SimpleHandlebarsTemplate -end \ No newline at end of file diff --git a/vendor/gems/simple_handlebars_rails/lib/simple_handlebars_rails/simple_handlebars_template.rb b/vendor/gems/simple_handlebars_rails/lib/simple_handlebars_rails/simple_handlebars_template.rb deleted file mode 100644 index 0486769a32e..00000000000 --- a/vendor/gems/simple_handlebars_rails/lib/simple_handlebars_rails/simple_handlebars_template.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'tilt/template' - -module SimpleHandlebarsRails - - # = Sprockets engine for MustacheTemplate templates - class SimpleHandlebarsTemplate < Tilt::Template - def self.default_mime_type - 'application/javascript' - end - - def initialize_engine - end - - def prepare - end - - # Generates Javascript code from a HandlebarsJS template. - # The SC template name is derived from the lowercase logical asset path - # by replacing non-alphanum characheters by underscores. - def evaluate(scope, locals, &block) - - template = data.dup - template.gsub!(/"/, '\\"') - template.gsub!(/\r?\n/, '\\n') - template.gsub!(/\t/, '\\t') - - # TODO: make this an option - templateName = scope.logical_path.downcase.gsub(/[^a-z0-9\/]/, '_') - templateName.gsub!(/^discourse\/templates\//, '') - - # TODO precompile so we can just have handlebars-runtime in prd - - result = "if (typeof HANDLEBARS_TEMPLATES == 'undefined') HANDLEBARS_TEMPLATES = {};\n" - result << "HANDLEBARS_TEMPLATES[\"#{templateName}\"] = Handlebars.compile(\"#{template}\");\n" - result - end - end -end diff --git a/vendor/gems/simple_handlebars_rails/simple_handlebars_rails.gemspec b/vendor/gems/simple_handlebars_rails/simple_handlebars_rails.gemspec deleted file mode 100644 index 6c35f4a9732..00000000000 --- a/vendor/gems/simple_handlebars_rails/simple_handlebars_rails.gemspec +++ /dev/null @@ -1,17 +0,0 @@ -# -*- encoding: utf-8 -*- -$:.push File.expand_path("../lib", __FILE__) - -Gem::Specification.new do |s| - s.name = "simple_handlebars_rails" - s.version = "0.0.1" - s.authors = ["Robin Ward"] - s.email = ["robin.ward@gmail.com"] - s.homepage = "" - s.summary = %q{Basic Mustache Support for Rails} - s.description = %q{Adds the Mustache plugin and a corresponding Sprockets engine to the asset pipeline in Rails applications.} - - s.add_dependency "rails", ["> 3.1"] - - s.files = Dir["lib/**/*"] - s.require_paths = ["lib"] -end