diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js index 7c3cefc1e55..33b57894a7f 100644 --- a/app/assets/javascripts/discourse/models/category.js +++ b/app/assets/javascripts/discourse/models/category.js @@ -77,7 +77,8 @@ Discourse.Category = Discourse.Model.extend({ background_url: this.get('background_url'), allow_badges: this.get('allow_badges'), custom_fields: this.get('custom_fields'), - topic_template: this.get('topic_template') + topic_template: this.get('topic_template'), + suppress_from_homepage: this.get('suppress_from_homepage'), }, type: this.get('id') ? 'PUT' : 'POST' }); diff --git a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 index 185c2fd529a..0011b030554 100644 --- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 @@ -40,7 +40,6 @@ function findTopicList(store, filter, filterParams, extras) { session.setProperties({topicList: null, topicListScrollPosition: null}); } - // Clean up any string parameters that might slip through filterParams = filterParams || {}; Ember.keys(filterParams).forEach(function(k) { @@ -50,17 +49,7 @@ function findTopicList(store, filter, filterParams, extras) { } }); - const findParams = {}; - Discourse.SiteSettings.top_menu.split('|').forEach(function (i) { - if (i.indexOf(filter) === 0) { - const exclude = i.split("-"); - if (exclude && exclude.length === 2) { - findParams.exclude_category = exclude[1]; - } - } - }); - return resolve(store.findFiltered('topicList', { filter, params:_.extend(findParams, filterParams || {})})); - + return resolve(store.findFiltered('topicList', { filter, params: filterParams || {} })); }).then(function(list) { list.set('listParams', filterParams); if (tracking) { diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs index 9dab35610f3..dd943f94944 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs @@ -44,4 +44,11 @@ {{/if}} +
+ +
+ {{plugin-outlet "category-custom-settings"}} diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 10500fc4115..9d3ac191e9c 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -16,6 +16,7 @@ class CategoriesController < ApplicationController options = {} options[:latest_posts] = params[:latest_posts] || SiteSetting.category_featured_topics options[:parent_category_id] = params[:parent_category_id] + options[:is_homepage] = current_homepage == "categories".freeze @list = CategoryList.new(guardian, options) @list.draft_key = Draft::NEW_TOPIC @@ -24,7 +25,7 @@ class CategoriesController < ApplicationController discourse_expires_in 1.minute - unless current_homepage == 'categories' + unless current_homepage == "categories" @title = I18n.t('js.filters.categories.title') end @@ -139,6 +140,7 @@ class CategoriesController < ApplicationController :position, :email_in, :email_in_allow_strangers, + :suppress_from_homepage, :parent_category_id, :auto_close_hours, :auto_close_based_on_last_post, diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index e5b6f638879..d894ee1f37c 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -5,36 +5,34 @@ class ListController < ApplicationController skip_before_filter :check_xhr - @@categories = [ + before_filter :set_category, only: [ # filtered topics lists - Discourse.filters.map { |f| "category_#{f}".to_sym }, - Discourse.filters.map { |f| "category_none_#{f}".to_sym }, - Discourse.filters.map { |f| "parent_category_category_#{f}".to_sym }, - Discourse.filters.map { |f| "parent_category_category_none_#{f}".to_sym }, + Discourse.filters.map { |f| :"category_#{f}" }, + Discourse.filters.map { |f| :"category_none_#{f}" }, + Discourse.filters.map { |f| :"parent_category_category_#{f}" }, + Discourse.filters.map { |f| :"parent_category_category_none_#{f}" }, # top summaries :category_top, :category_none_top, :parent_category_category_top, # top pages (ie. with a period) - TopTopic.periods.map { |p| "category_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "category_none_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "parent_category_category_top_#{p}".to_sym }, + TopTopic.periods.map { |p| :"category_top_#{p}" }, + TopTopic.periods.map { |p| :"category_none_top_#{p}" }, + TopTopic.periods.map { |p| :"parent_category_category_top_#{p}" }, # category feeds :category_feed, ].flatten - before_filter :set_category, only: @@categories - before_filter :ensure_logged_in, except: [ :topics_by, # anonymous filters Discourse.anonymous_filters, - Discourse.anonymous_filters.map { |f| "#{f}_feed".to_sym }, + Discourse.anonymous_filters.map { |f| "#{f}_feed" }, # anonymous categorized filters - Discourse.anonymous_filters.map { |f| "category_#{f}".to_sym }, - Discourse.anonymous_filters.map { |f| "category_none_#{f}".to_sym }, - Discourse.anonymous_filters.map { |f| "parent_category_category_#{f}".to_sym }, - Discourse.anonymous_filters.map { |f| "parent_category_category_none_#{f}".to_sym }, + Discourse.anonymous_filters.map { |f| :"category_#{f}" }, + Discourse.anonymous_filters.map { |f| :"category_none_#{f}" }, + Discourse.anonymous_filters.map { |f| :"parent_category_category_#{f}" }, + Discourse.anonymous_filters.map { |f| :"parent_category_category_none_#{f}" }, # category feeds :category_feed, # top summaries @@ -43,14 +41,14 @@ class ListController < ApplicationController :category_none_top, :parent_category_category_top, # top pages (ie. with a period) - TopTopic.periods.map { |p| "top_#{p}".to_sym }, - TopTopic.periods.map { |p| "category_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "category_none_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "parent_category_category_top_#{p}".to_sym }, + TopTopic.periods.map { |p| :"top_#{p}" }, + TopTopic.periods.map { |p| :"category_top_#{p}" }, + TopTopic.periods.map { |p| :"category_none_top_#{p}" }, + TopTopic.periods.map { |p| :"parent_category_category_top_#{p}" }, ].flatten # Create our filters - Discourse.filters.each_with_index do |filter, idx| + Discourse.filters.each do |filter| define_method(filter) do |options = nil| list_opts = build_topic_list_options list_opts.merge!(options) if options @@ -60,6 +58,10 @@ class ListController < ApplicationController list_opts[:no_definitions] = true end + if filter.to_s == current_homepage + list_opts.merge!(exclude_category_ids: get_excluded_category_ids(list_opts[:category])) + end + list = TopicQuery.new(user, list_opts).public_send("list_#{filter}") list.more_topics_url = construct_url_with(:next, list_opts) list.prev_topics_url = construct_url_with(:prev, list_opts) @@ -83,34 +85,20 @@ class ListController < ApplicationController define_method("category_#{filter}") do canonical_url "#{Discourse.base_url}#{@category.url}" - self.send(filter, { category: @category.id }) + self.send(filter, category: @category.id) end define_method("category_none_#{filter}") do - self.send(filter, { category: @category.id, no_subcategories: true }) + self.send(filter, category: @category.id, no_subcategories: true) end define_method("parent_category_category_#{filter}") do canonical_url "#{Discourse.base_url}#{@category.url}" - self.send(filter, { category: @category.id }) + self.send(filter, category: @category.id) end define_method("parent_category_category_none_#{filter}") do - self.send(filter, { category: @category.id }) - end - end - - Discourse.feed_filters.each do |filter| - define_method("#{filter}_feed") do - discourse_expires_in 1.minute - - @title = "#{SiteSetting.title} - #{I18n.t("rss_description.#{filter}")}" - @link = "#{Discourse.base_url}/#{filter}" - @description = I18n.t("rss_description.#{filter}") - @atom_link = "#{Discourse.base_url}/#{filter}.rss" - @topic_list = TopicQuery.new(nil, order: 'created').public_send("list_#{filter}") - - render 'list', formats: [:rss] + self.send(filter, category: @category.id) end end @@ -127,14 +115,26 @@ class ListController < ApplicationController end end + def latest_feed + discourse_expires_in 1.minute + + @title = "#{SiteSetting.title} - #{I18n.t("rss_description.latest")}" + @link = "#{Discourse.base_url}/latest" + @atom_link = "#{Discourse.base_url}/latest.rss" + @description = I18n.t("rss_description.latest") + @topic_list = TopicQuery.new(nil, order: 'created').list_latest + + render 'list', formats: [:rss] + end + def category_feed guardian.ensure_can_see!(@category) discourse_expires_in 1.minute @title = @category.name @link = "#{Discourse.base_url}#{@category.url}" - @description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}" @atom_link = "#{Discourse.base_url}#{@category.url}.rss" + @description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}" @topic_list = TopicQuery.new.list_new_in_category(@category) render 'list', formats: [:rss] @@ -147,15 +147,15 @@ class ListController < ApplicationController end def category_top - top({ category: @category.id }) + top(category: @category.id) end def category_none_top - top({ category: @category.id, no_subcategories: true }) + top(category: @category.id, no_subcategories: true) end def parent_category_category_top - top({ category: @category.id }) + top(category: @category.id) end TopTopic.periods.each do |period| @@ -163,6 +163,11 @@ class ListController < ApplicationController top_options = build_topic_list_options top_options.merge!(options) if options top_options[:per_page] = SiteSetting.topics_per_period_in_top_page + + if "top".freeze == current_homepage + top_options.merge!(exclude_category_ids: get_excluded_category_ids(top_options[:category])) + end + user = list_target_user list = TopicQuery.new(user, top_options).list_top_for(period) list.for_period = period @@ -177,15 +182,15 @@ class ListController < ApplicationController end define_method("category_top_#{period}") do - self.send("top_#{period}", { category: @category.id }) + self.send("top_#{period}", category: @category.id) end define_method("category_none_top_#{period}") do - self.send("top_#{period}", { category: @category.id, no_subcategories: true }) + self.send("top_#{period}", category: @category.id, no_subcategories: true) end define_method("parent_category_category_top_#{period}") do - self.send("top_#{period}", { category: @category.id }) + self.send("top_#{period}", category: @category.id) end end @@ -204,16 +209,15 @@ class ListController < ApplicationController end end - private def page_params(opts = nil) opts ||= {} - route_params = {format: 'json'} - route_params[:category] = @category.slug_for_url if @category + route_params = { format: 'json' } + route_params[:category] = @category.slug_for_url if @category route_params[:parent_category] = @category.parent_category.slug_for_url if @category && @category.parent_category - route_params[:order] = opts[:order] if opts[:order].present? - route_params[:ascending] = opts[:ascending] if opts[:ascending].present? + route_params[:order] = opts[:order] if opts[:order].present? + route_params[:ascending] = opts[:ascending] if opts[:ascending].present? route_params end @@ -235,11 +239,10 @@ class ListController < ApplicationController end def build_topic_list_options - # exclude_category = 1. from params / 2. parsed from top menu / 3. nil options = { page: params[:page], topic_ids: param_to_integer_list(:topic_ids), - exclude_category: (params[:exclude_category] || select_menu_item.try(:filter)), + exclude_category_ids: params[:exclude_category_ids], category: params[:category], order: params[:order], ascending: params[:ascending], @@ -257,17 +260,6 @@ class ListController < ApplicationController options end - def select_menu_item - menu_item = SiteSetting.top_menu_items.select do |mu| - (mu.has_specific_category? && mu.specific_category == @category.try(:slug)) || - action_name == mu.name || - (action_name.include?("top") && mu.name == "top") - end.first - - menu_item = nil if menu_item.try(:has_specific_category?) && menu_item.specific_category == @category.try(:slug) - menu_item - end - def list_target_user if params[:user_id] && guardian.is_staff? User.find(params[:user_id].to_i) @@ -290,25 +282,16 @@ class ListController < ApplicationController url.sub('.json?','?') end - def generate_top_lists(options) - top = TopLists.new - - options[:per_page] = SiteSetting.topics_per_period_in_top_summary - topic_query = TopicQuery.new(current_user, options) - - periods = [ListController.best_period_for(current_user.try(:previous_visit_at), options[:category])] - - periods.each { |period| top.send("#{period}=", topic_query.list_top_for(period)) } - - top + def get_excluded_category_ids(current_category=nil) + exclude_category_ids = Category.where(suppress_from_homepage: true) + exclude_category_ids = exclude_category_ids.where.not(id: current_category) if current_category + exclude_category_ids.pluck(:id) end def self.best_period_for(previous_visit_at, category_id=nil) best_periods_for(previous_visit_at).each do |period| top_topics = TopTopic.where("#{period}_score > 0") - if category_id - top_topics = top_topics.joins(:topic).where("topics.category_id = ?", category_id) - end + top_topics = top_topics.joins(:topic).where("topics.category_id = ?", category_id) if category_id return period if top_topics.count >= SiteSetting.topics_per_period_in_top_page end # default period is yearly @@ -318,8 +301,8 @@ class ListController < ApplicationController def self.best_periods_for(date) date ||= 1.year.ago periods = [] - periods << :daily if date > 8.days.ago - periods << :weekly if date > 35.days.ago + periods << :daily if date > 8.days.ago + periods << :weekly if date > 35.days.ago periods << :monthly if date > 180.days.ago periods << :yearly periods diff --git a/app/models/category_list.rb b/app/models/category_list.rb index b5cae822b87..2e32369fda1 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -66,6 +66,8 @@ class CategoryList @categories = @categories.where('categories.parent_category_id = ?', @options[:parent_category_id].to_i) end + @categories = @categories.where(suppress_from_homepage: false) if @options[:is_homepage] + if SiteSetting.fixed_category_positions @categories = @categories.order('position ASC').order('id ASC') else diff --git a/app/serializers/category_serializer.rb b/app/serializers/category_serializer.rb index 1757652094b..1bf08f89fcb 100644 --- a/app/serializers/category_serializer.rb +++ b/app/serializers/category_serializer.rb @@ -8,6 +8,7 @@ class CategorySerializer < BasicCategorySerializer :position, :email_in, :email_in_allow_strangers, + :suppress_from_homepage, :can_delete, :cannot_delete_reason, :allow_badges, @@ -56,4 +57,8 @@ class CategorySerializer < BasicCategorySerializer scope && scope.can_edit?(object) end + def include_suppress_from_homepage? + scope && scope.can_edit?(object) + end + end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 76feed42e2d..7f4e13300e9 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1539,6 +1539,7 @@ en: email_in_allow_strangers: "Accept emails from anonymous users with no accounts" email_in_disabled: "Posting new topics via email is disabled in the Site Settings. To enable posting new topics via email, " email_in_disabled_click: 'enable the "email in" setting.' + suppress_from_homepage: "Suppress this category from the homepage." allow_badges_label: "Allow badges to be awarded in this category" edit_permissions: "Edit Permissions" add_permission: "Add Permission" diff --git a/db/migrate/20150828155137_add_suppress_from_homepage_to_category.rb b/db/migrate/20150828155137_add_suppress_from_homepage_to_category.rb new file mode 100644 index 00000000000..e706f40f97c --- /dev/null +++ b/db/migrate/20150828155137_add_suppress_from_homepage_to_category.rb @@ -0,0 +1,5 @@ +class AddSuppressFromHomepageToCategory < ActiveRecord::Migration + def change + add_column :categories, :suppress_from_homepage, :boolean, default: false + end +end diff --git a/lib/discourse.rb b/lib/discourse.rb index b32fcf736aa..82b1280eed2 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -64,18 +64,10 @@ module Discourse @filters ||= [:latest, :unread, :new, :read, :posted, :bookmarks] end - def self.feed_filters - @feed_filters ||= [:latest] - end - def self.anonymous_filters @anonymous_filters ||= [:latest, :top, :categories] 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 + [:category, :categories, :top] end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 9ffa5d8b598..25e2b6fd488 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -1,15 +1,16 @@ # -# Helps us find topics. Returns a TopicList object containing the topics -# found. +# Helps us find topics. +# Returns a TopicList object containing the topics found. # + require_dependency 'topic_list' require_dependency 'suggested_topics_builder' 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_ids - exclude_category + VALID_OPTIONS = %i(except_topic_ids + exclude_category_ids limit page per_page @@ -27,8 +28,7 @@ class TopicQuery search slow_platform filter - q - ).map(&:to_sym) + q) # Maps `order` to a columns in `topics` SORTABLE_MAPPING = { @@ -301,14 +301,17 @@ class TopicQuery if options[:no_subcategories] result = result.where('categories.id = ?', category_id) else - result = result.where('categories.id = ? or (categories.parent_category_id = ? AND categories.topic_id <> topics.id)', category_id, category_id) + result = result.where('categories.id = :category_id OR (categories.parent_category_id = :category_id AND categories.topic_id <> topics.id)', category_id: category_id) end result = result.references(:categories) end result = apply_ordering(result, options) result = result.listable_topics.includes(:category) - result = result.where('categories.name is null or categories.name <> ?', options[:exclude_category]).references(:categories) if options[:exclude_category] + + if options[:exclude_category_ids] && options[:exclude_category_ids].is_a?(Array) && options[:exclude_category_ids].size > 0 + result = result.where("categories.id NOT IN (?)", options[:exclude_category_ids]).references(:categories) + end # Don't include the category topics if excluded if options[:no_definitions] @@ -393,19 +396,20 @@ class TopicQuery def remove_muted_categories(list, user, opts=nil) category_id = get_category_id(opts[:exclude]) if opts + if user - list = list.where("NOT EXISTS( - SELECT 1 FROM category_users cu - WHERE cu.user_id = ? AND - cu.category_id = topics.category_id AND - cu.notification_level = ? AND - cu.category_id <> ? - )", - user.id, - CategoryUser.notification_levels[:muted], - category_id || -1 - ) - .references('cu') + list = list.references("cu") + .where(" + NOT EXISTS ( + SELECT 1 + FROM category_users cu + WHERE cu.user_id = :user_id + AND cu.category_id = topics.category_id + AND cu.notification_level = :muted + AND cu.category_id <> :category_id + )", user_id: user.id, + muted: CategoryUser.notification_levels[:muted], + category_id: category_id || -1) end list diff --git a/spec/controllers/list_controller_spec.rb b/spec/controllers/list_controller_spec.rb index 6f55de5b607..41f343ca10b 100644 --- a/spec/controllers/list_controller_spec.rb +++ b/spec/controllers/list_controller_spec.rb @@ -32,12 +32,6 @@ describe ListController do end end - Discourse.logged_in_filters.each do |filter| - context "#{filter}" do - it { expect { xhr :get, filter }.to raise_error(Discourse::NotLoggedIn) } - end - end - it 'allows users to filter on a set of topic ids' do p = create_post @@ -51,14 +45,10 @@ describe ListController do describe 'RSS feeds' do - Discourse.feed_filters.each do |filter| - - it 'renders RSS' do - get "#{filter}_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - + it 'renders RSS' do + get "latest_feed", format: :rss + expect(response).to be_success + expect(response.content_type).to eq('application/rss+xml') end end