diff --git a/app/assets/javascripts/discourse/helpers/application.js.es6 b/app/assets/javascripts/discourse/helpers/application.js.es6 index dda9e4e7b99..4c414db6b46 100644 --- a/app/assets/javascripts/discourse/helpers/application.js.es6 +++ b/app/assets/javascripts/discourse/helpers/application.js.es6 @@ -24,7 +24,7 @@ registerUnbound('number', (orig, params) => { // Round off the thousands to one decimal place const n = number(orig); - if (n !== title) { + if (n.toString() !== title.toString() && !params.noTitle) { result += " title='" + Handlebars.Utils.escapeExpression(title) + "'"; } result += ">" + n + ""; diff --git a/app/assets/javascripts/discourse/models/category-list.js.es6 b/app/assets/javascripts/discourse/models/category-list.js.es6 index c1469c314d7..5eccd98143c 100644 --- a/app/assets/javascripts/discourse/models/category-list.js.es6 +++ b/app/assets/javascripts/discourse/models/category-list.js.es6 @@ -14,6 +14,17 @@ CategoryList.reopenClass({ const users = Discourse.Model.extractByKey(result.featured_users, Discourse.User); const list = Discourse.Category.list(); + let statPeriod; + const minCategories = result.category_list.categories.length * 0.8; + + ["week", "month"].some(period => { + const filteredCategories = result.category_list.categories.filter(c => c[`topics_${period}`] > 0); + if (filteredCategories.length >= minCategories) { + statPeriod = period; + return true; + } + }); + result.category_list.categories.forEach(c => { if (c.parent_category_id) { c.parentCategory = list.findBy('id', c.parent_category_id); @@ -31,6 +42,22 @@ CategoryList.reopenClass({ c.topics = c.topics.map(t => Discourse.Topic.create(t)); } + switch(statPeriod) { + case "week": + case "month": + const stat = c[`topics_${statPeriod}`]; + const unit = I18n.t(statPeriod); + if (stat > 0) { + c.stat = `${stat} / ${unit}`; + c.statTitle = I18n.t("categories.topic_stat_sentence", { count: stat, unit: unit }); + break; + } + default: + c.stat = `${c.topic_count}`; + c.statTitle = I18n.t("categories.topic_sentence", { count: c.topic_count }); + break; + } + categories.pushObject(store.createRecord('category', c)); }); return categories; diff --git a/app/assets/javascripts/discourse/models/category.js.es6 b/app/assets/javascripts/discourse/models/category.js.es6 index 000aeaeccb4..724a2664699 100644 --- a/app/assets/javascripts/discourse/models/category.js.es6 +++ b/app/assets/javascripts/discourse/models/category.js.es6 @@ -1,5 +1,6 @@ import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; +import computed from 'ember-addons/ember-computed-decorators'; import { on } from 'ember-addons/ember-computed-decorators'; import PermissionType from 'discourse/models/permission-type'; @@ -17,56 +18,64 @@ const Category = RestModel.extend({ availableGroups.removeObject(elem.group_name); return { group_name: elem.group_name, - permission: PermissionType.create({id: elem.permission_type}) + permission: PermissionType.create({ id: elem.permission_type }) }; })); } }, - availablePermissions: function(){ - return [ PermissionType.create({id: PermissionType.FULL}), - PermissionType.create({id: PermissionType.CREATE_POST}), - PermissionType.create({id: PermissionType.READONLY}) - ]; - }.property(), + @computed + availablePermissions() { + return [ + PermissionType.create({ id: PermissionType.FULL }), + PermissionType.create({ id: PermissionType.CREATE_POST }), + PermissionType.create({ id: PermissionType.READONLY }) + ]; + }, - searchContext: function() { - return ({ type: 'category', id: this.get('id'), category: this }); - }.property('id'), + @computed("id") + searchContext(id) { + return { type: 'category', id, category: this }; + }, - url: function() { + @computed("name") + url() { return Discourse.getURL("/c/") + Category.slugFor(this); - }.property('name'), + }, - fullSlug: function() { - return this.get("url").slice(3).replace("/", "-"); - }.property("url"), + @computed("url") + fullSlug(url) { + return url.slice(3).replace("/", "-"); + }, - nameLower: function() { - return this.get('name').toLowerCase(); - }.property('name'), + @computed("name") + nameLower(name) { + return name.toLowerCase(); + }, - unreadUrl: function() { - return this.get('url') + '/l/unread'; - }.property('url'), + @computed("url") + unreadUrl(url) { + return `${url}/l/unread`; + }, - newUrl: function() { - return this.get('url') + '/l/new'; - }.property('url'), + @computed("url") + newUrl(url) { + return `${url}/l/new`; + }, - style: function() { - return "background-color: #" + this.get('category.color') + "; color: #" + this.get('category.text_color') + ";"; - }.property('color', 'text_color'), + @computed("color", "text_color") + style(color, textColor) { + return `background-color: #${color}; color: #${textColor}`; + }, - moreTopics: function() { - return this.get('topic_count') > Discourse.SiteSettings.category_featured_topics; - }.property('topic_count'), + @computed("topic_count") + moreTopics(topicCount) { + return topicCount > Discourse.SiteSettings.category_featured_topics; + }, - save: function() { - var url = "/categories"; - if (this.get('id')) { - url = "/categories/" + this.get('id'); - } + save() { + const id = this.get("id"); + const url = id ? `/categories/${id}` : "/categories"; return ajax(url, { data: { @@ -91,111 +100,74 @@ const Category = RestModel.extend({ allowed_tags: this.get('allowed_tags'), allowed_tag_groups: this.get('allowed_tag_groups') }, - type: this.get('id') ? 'PUT' : 'POST' + type: id ? 'PUT' : 'POST' }); }, - permissionsForUpdate: function(){ - var rval = {}; - _.each(this.get("permissions"),function(p){ - rval[p.group_name] = p.permission.id; - }); + @computed("permissions") + permissionsForUpdate(permissions) { + let rval = {}; + permissions.forEach(p => rval[p.group_name] = p.permission.id); return rval; - }.property("permissions"), - - destroy: function() { - return ajax("/categories/" + (this.get('id') || this.get('slug')), { type: 'DELETE' }); }, - addPermission: function(permission){ + destroy() { + return ajax(`/categories/${this.get('id') || this.get('slug')}`, { type: 'DELETE' }); + }, + + addPermission(permission) { this.get("permissions").addObject(permission); this.get("availableGroups").removeObject(permission.group_name); }, - - removePermission: function(permission){ + removePermission(permission) { this.get("permissions").removeObject(permission); this.get("availableGroups").addObject(permission.group_name); }, - permissions: function(){ + @computed + permissions() { return Em.A([ - {group_name: "everyone", permission: PermissionType.create({id: 1})}, - {group_name: "admins", permission: PermissionType.create({id: 2}) }, - {group_name: "crap", permission: PermissionType.create({id: 3}) } + { group_name: "everyone", permission: PermissionType.create({id: 1}) }, + { group_name: "admins", permission: PermissionType.create({id: 2}) }, + { group_name: "crap", permission: PermissionType.create({id: 3}) } ]); - }.property(), + }, - latestTopic: function(){ - var topics = this.get('topics'); + @computed("topics") + latestTopic(topics) { if (topics && topics.length) { return topics[0]; } - }.property("topics"), + }, - featuredTopics: function() { - var topics = this.get('topics'); + @computed("topics") + featuredTopics(topics) { if (topics && topics.length) { return topics.slice(0, Discourse.SiteSettings.category_featured_topics || 2); } - }.property('topics'), + }, - unreadTopics: function() { - return this.topicTrackingState.countUnread(this.get('id')); - }.property('topicTrackingState.messageCount'), + @computed("id", "topicTrackingState.messageCount") + unreadTopics(id) { + return this.topicTrackingState.countUnread(id); + }, - newTopics: function() { - return this.topicTrackingState.countNew(this.get('id')); - }.property('topicTrackingState.messageCount'), + @computed("id", "topicTrackingState.messageCount") + newTopics(id) { + return this.topicTrackingState.countNew(id); + }, - topicStatsTitle: function() { - var string = I18n.t('categories.topic_stats'); - _.each(this.get('topicCountStats'), function(stat) { - string += ' ' + I18n.t('categories.topic_stat_sentence', {count: stat.value, unit: stat.unit}); - }, this); - return string; - }.property('post_count'), - - postStatsTitle: function() { - var string = I18n.t('categories.post_stats'); - _.each(this.get('postCountStats'), function(stat) { - string += ' ' + I18n.t('categories.post_stat_sentence', {count: stat.value, unit: stat.unit}); - }, this); - return string; - }.property('post_count'), - - topicCountStats: function() { - return this.countStats('topics'); - }.property('topics_year', 'topics_month', 'topics_week', 'topics_day'), - - setNotification: function(notification_level) { - var url = "/category/" + this.get('id')+"/notifications"; + setNotification(notification_level) { this.set('notification_level', notification_level); - return ajax(url, { - data: { - notification_level: notification_level - }, - type: 'POST' - }); + const url = `/category/${this.get('id')}/notifications`; + return ajax(url, { data: { notification_level }, type: 'POST' }); }, - postCountStats: function() { - return this.countStats('posts'); - }.property('posts_year', 'posts_month', 'posts_week', 'posts_day'), - - countStats: function(prefix) { - var stats = [], val; - _.each(['day', 'week', 'month', 'year'], function(unit) { - val = this.get(prefix + '_' + unit); - if (val > 0) stats.pushObject({value: val, unit: I18n.t(unit)}); - if (stats.length === 2) return false; - }, this); - return stats; - }, - - isUncategorizedCategory: function() { - return this.get('id') === Discourse.Site.currentProp("uncategorized_category_id"); - }.property('id') + @computed("id") + isUncategorizedCategory(id) { + return id === Discourse.Site.currentProp("uncategorized_category_id"); + } }); var _uncategorized; diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 712a1b3690a..a6184e3680f 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -71,6 +71,11 @@ const Topic = RestModel.extend({ I18n.t('last_post') + ": " + longDate(this.get('bumpedAt')); }.property('bumpedAt'), + @computed('replyCount') + replyTitle(count) { + return I18n.t("posts_likes", { count }); + }, + createdAt: function() { return new Date(this.get('created_at')); }.property('created_at'), diff --git a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 index 4aa3e17a5f6..d33919cbb62 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 @@ -2,7 +2,7 @@ import showModal from "discourse/lib/show-modal"; import OpenComposer from "discourse/mixins/open-composer"; import CategoryList from "discourse/models/category-list"; import { defaultHomepage } from 'discourse/lib/utilities'; -import PreloadStore from 'preload-store'; +import TopicList from "discourse/models/topic-list"; const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { renderTemplate() { @@ -15,10 +15,6 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { }, model() { - // TODO: Remove this and ensure server side does not supply `topic_list` - // if default page is categories - PreloadStore.remove("topic_list"); - return CategoryList.list(this.store, 'categories').then(list => { const tracking = this.topicTrackingState; if (tracking) { @@ -35,6 +31,8 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { }, setupController(controller, model) { + TopicList.find("latest").then(result => model.set("topicList", result)); + controller.set("model", model); this.controllerFor("navigation/categories").setProperties({ diff --git a/app/assets/javascripts/discourse/templates/discovery/categories.hbs b/app/assets/javascripts/discourse/templates/discovery/categories.hbs index c4fb4fa6777..00dcd4973dc 100644 --- a/app/assets/javascripts/discourse/templates/discovery/categories.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/categories.hbs @@ -1,10 +1,9 @@ {{#if model.categories}} {{#discovery-categories refresh="refresh"}} - +
- @@ -18,7 +17,6 @@ {{#if c.logo_url}} {{category-logo-link category=c}} {{/if}} -
{{{c.description_excerpt}}}
@@ -33,27 +31,66 @@ {{/if}} - - {{/each}}
{{i18n 'categories.category'}}{{i18n 'categories.latest'}} {{i18n 'categories.topics'}}
- {{#each c.featuredTopics as |f|}} - {{featured-topic topic=f latestTopicOnly=latestTopicOnly action="showTopicEntrance"}} - {{/each}} - - - - {{#each c.topicCountStats as |s|}} - - - - - {{/each}} - -
{{s.value}} / {{s.unit}}
+
+ {{{c.stat}}}
{{/discovery-categories}} - {{/if}} + + + + + + + + {{#each model.topicList.topics as |t|}} + +
{{i18n "filters.latest.title"}}
+ + + + + {{topic-status topic=t}} + {{topic-link t}} + {{#if t.unseen}} + + {{/if}} + + + {{category-link t.category}} + {{#if t.tags}} + {{#each t.visibleListTags as |tag|}} + {{discourse-tag tag}} + {{/each}} + {{/if}} + + + + + +
+ {{#with t.posters.lastObject.user as |lastPoster|}} + {{#user-link user=lastPoster}} + {{avatar lastPoster imageSize="large"}} + {{/user-link}} + {{/with}} +
+ + +
+ + {{else}} + {{loading-spinner}} + {{/each}} + + +
diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index d77a94e11dd..20d6a8d1617 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -107,6 +107,41 @@ html.anon .topic-list a.title:visited:not(.badge-notification) {color: dark-ligh } +.navigation-categories { + .topic-list { + width: 48%; + float: left; + } + .main-link { + width: 100%; + .discourse-tag { + font-size: 12px; + } + } + .topic-stats { + text-align: right; + a { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 40%)); + } + } + .topic-replies { + font-weight: bold; + margin-bottom: 10px; + } + .topic-list-latest { + margin-left: 4%; + } + .topic-list.categories { + th.stats { + width: 20%; + } + .stats { + vertical-align: top; + text-align: center; + } + } +} + .topic-list.categories { diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index 21a438dc334..1cca25a7abf 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -169,6 +169,7 @@ header .discourse-tag {color: $tag-color !important; } .bullet + .list-tags { display: block; + line-height: 15px; } .bar + .list-tags { @@ -247,4 +248,4 @@ header .discourse-tag {color: $tag-color !important; } margin-right: 10px; } } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 3dd2d80eb44..77330e382c4 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -55,6 +55,8 @@ display: inline-flex; align-items: baseline; margin-right: 10px; + font-size: 12px; + line-height: 15px; span.badge-category { color: $primary !important; diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index b9059b0dd43..0df2a15fe3b 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -193,8 +193,6 @@ } } .category{ - width: 45%; - h3 { display: inline-block; font-size: 1.286em; diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 8617970c820..463e0084a96 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -12,28 +12,29 @@ class CategoriesController < ApplicationController end def index - @description = SiteSetting.site_description - - 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 - @list.draft_sequence = DraftSequence.current(current_user, Draft::NEW_TOPIC) - @list.draft = Draft.get(current_user, @list.draft_key, @list.draft_sequence) if current_user - discourse_expires_in 1.minute - unless current_homepage == "categories" - @title = I18n.t('js.filters.categories.title') - end + @description = SiteSetting.site_description + + category_options = { is_homepage: current_homepage == "categories".freeze } + + @category_list = CategoryList.new(guardian, category_options) + @category_list.draft_key = Draft::NEW_TOPIC + @category_list.draft_sequence = DraftSequence.current(current_user, Draft::NEW_TOPIC) + @category_list.draft = Draft.get(current_user, @category_list.draft_key, @category_list.draft_sequence) if current_user + + @title = I18n.t('js.filters.categories.title') unless category_options[:is_homepage] - store_preloaded("categories_list", MultiJson.dump(CategoryListSerializer.new(@list, scope: guardian))) respond_to do |format| - format.html { render } - format.json { render_serialized(@list, CategoryListSerializer) } + format.html do + topic_options = { per_page: SiteSetting.categories_topics, no_definitions: true } + topic_list = TopicQuery.new(current_user, topic_options).list_latest + store_preloaded(topic_list.preload_key, MultiJson.dump(TopicListSerializer.new(topic_list, scope: guardian))) + store_preloaded(@category_list.preload_key, MultiJson.dump(CategoryListSerializer.new(@category_list, scope: guardian))) + render + end + + format.json { render_serialized(@category_list, CategoryListSerializer) } end end diff --git a/app/models/category.rb b/app/models/category.rb index 9b23f729dea..a52cdeff3f3 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -87,7 +87,7 @@ class Category < ActiveRecord::Base # permission is just used by serialization # we may consider wrapping this in another spot - attr_accessor :displayable_topics, :permission, :subcategory_ids, :notification_level, :has_children + attr_accessor :permission, :subcategory_ids, :notification_level, :has_children @topic_id_cache = DistributedCache.new('category_topic_ids') @@ -187,9 +187,7 @@ SQL self.topic_id ? query.where(['topics.id <> ?', self.topic_id]) : query end - - # Internal: Generate the text of post prompting to enter category - # description. + # Internal: Generate the text of post prompting to enter category description. def self.post_template I18n.t("category.post_template", replace_paragraph: I18n.t("category.replace_paragraph")) end @@ -219,7 +217,6 @@ SQL @@cache.getset(self.description) do Nokogiri::HTML(self.description).text end - end def duplicate_slug? diff --git a/app/models/category_list.rb b/app/models/category_list.rb index b6ffafb76af..38f9fc14f77 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -1,75 +1,37 @@ -require_dependency 'pinned_check' - class CategoryList include ActiveModel::Serialization attr_accessor :categories, - :topic_users, :uncategorized, :draft, :draft_key, :draft_sequence - def initialize(guardian=nil, options = {}) + def initialize(guardian=nil, options={}) @guardian = guardian || Guardian.new @options = options - find_relevant_topics unless latest_post_only? find_categories + end - prune_empty - find_user_data - sort_unpinned - trim_results + def preload_key + "categories_list".freeze end private - def latest_post_only? - @options[:latest_posts] and latest_posts_count == 1 - end - - def include_latest_posts? - @options[:latest_posts] and latest_posts_count > 1 - end - - def latest_posts_count - @options[:latest_posts].to_i > 0 ? @options[:latest_posts].to_i : SiteSetting.category_featured_topics - end - - # Retrieve a list of all the topics we'll need - def find_relevant_topics - @topics_by_category_id = {} - category_featured_topics = CategoryFeaturedTopic.select([:category_id, :topic_id]).order(:rank) - @topics_by_id = {} - - @all_topics = Topic.where(id: category_featured_topics.map(&:topic_id)) - @all_topics = @all_topics.includes(:last_poster) if include_latest_posts? - @all_topics.each do |t| - t.include_last_poster = true if include_latest_posts? # hint for serialization - @topics_by_id[t.id] = t - end - - category_featured_topics.each do |cft| - @topics_by_category_id[cft.category_id] ||= [] - @topics_by_category_id[cft.category_id] << cft.topic_id - end - end - # Find a list of all categories to associate the topics with def find_categories - @categories = Category - .includes(:featured_users, :topic_only_relative_url, subcategories: [:topic_only_relative_url]) - .secured(@guardian) - - if @options[:parent_category_id].present? - @categories = @categories.where('categories.parent_category_id = ?', @options[:parent_category_id].to_i) - end - + @categories = Category.includes(:topic_only_relative_url, subcategories: [:topic_only_relative_url]).secured(@guardian) @categories = @categories.where(suppress_from_homepage: false) if @options[:is_homepage] + unless SiteSetting.allow_uncategorized_topics + # TODO: also make sure the uncategorized is empty + @categories = @categories.where("id <> #{SiteSetting.uncategorized_category_id}") + end + if SiteSetting.fixed_category_positions - @categories = @categories.order('position ASC').order('id ASC') + @categories = @categories.order(:position, :id) else @categories = @categories.order('COALESCE(categories.posts_week, 0) DESC') .order('COALESCE(categories.posts_month, 0) DESC') @@ -77,10 +39,6 @@ class CategoryList .order('id ASC') end - if latest_post_only? - @categories = @categories.includes(latest_post: { topic: :last_poster }) - end - @categories = @categories.to_a category_user = {} @@ -95,95 +53,18 @@ class CategoryList category.has_children = category.subcategories.present? end - if @options[:parent_category_id].blank? - subcategories = {} - to_delete = Set.new - @categories.each do |c| - if c.parent_category_id.present? - subcategories[c.parent_category_id] ||= [] - subcategories[c.parent_category_id] << c.id - to_delete << c - end - end - - if subcategories.present? - @categories.each do |c| - c.subcategory_ids = subcategories[c.id] - end - @categories.delete_if {|c| to_delete.include?(c) } - end - end - - if latest_post_only? - @all_topics = [] - @categories.each do |c| - if c.latest_post && c.latest_post.topic && @guardian.can_see?(c.latest_post.topic) - c.displayable_topics = [c.latest_post.topic] - topic = c.latest_post.topic - topic.include_last_poster = true # hint for serialization - @all_topics << topic - end - end - end - - if @topics_by_category_id - @categories.each do |c| - topics_in_cat = @topics_by_category_id[c.id] - if topics_in_cat.present? - c.displayable_topics = [] - topics_in_cat.each do |topic_id| - topic = @topics_by_id[topic_id] - if topic.present? && @guardian.can_see?(topic) - # topic.category is very slow under rails 4.2 - topic.association(:category).target = c - c.displayable_topics << topic - end - end - end - end - end - end - - - def prune_empty - unless SiteSetting.allow_uncategorized_topics - # HACK: Don't show uncategorized to anyone if not allowed - @categories.delete_if do |c| - c.uncategorized? && c.displayable_topics.blank? - end - end - end - - # Get forum topic user records if appropriate - def find_user_data - if @guardian.current_user && @all_topics.present? - topic_lookup = TopicUser.lookup_for(@guardian.current_user, @all_topics) - - # Attach some data for serialization to each topic - @all_topics.each { |ft| ft.user_data = topic_lookup[ft.id] } - end - end - - def sort_unpinned - if @guardian.current_user && @all_topics.present? - # Put unpinned topics at the end of the list - @categories.each do |c| - next if c.displayable_topics.blank? || c.displayable_topics.size <= latest_posts_count - unpinned = [] - c.displayable_topics.each do |t| - unpinned << t if t.pinned_at && PinnedCheck.unpinned?(t, t.user_data) - end - unless unpinned.empty? - c.displayable_topics = (c.displayable_topics - unpinned) + unpinned - end - end - end - end - - def trim_results + subcategories = {} + to_delete = Set.new @categories.each do |c| - next if c.displayable_topics.blank? - c.displayable_topics = c.displayable_topics[0,latest_posts_count] + if c.parent_category_id.present? + subcategories[c.parent_category_id] ||= [] + subcategories[c.parent_category_id] << c.id + to_delete << c + end end + + @categories.each { |c| c.subcategory_ids = subcategories[c.id] } + + @categories.delete_if { |c| to_delete.include?(c) } end end diff --git a/app/serializers/category_detailed_serializer.rb b/app/serializers/category_detailed_serializer.rb index 16b028ca066..d7a454b19f6 100644 --- a/app/serializers/category_detailed_serializer.rb +++ b/app/serializers/category_detailed_serializer.rb @@ -6,17 +6,10 @@ class CategoryDetailedSerializer < BasicCategorySerializer :topics_week, :topics_month, :topics_year, - :posts_day, - :posts_week, - :posts_month, - :posts_year, :description_excerpt, :is_uncategorized, :subcategory_ids - has_many :featured_users, serializer: BasicUserSerializer - has_many :displayable_topics, serializer: ListableTopicSerializer, embed: :objects, key: :topics - def is_uncategorized object.id == SiteSetting.uncategorized_category_id end @@ -25,20 +18,14 @@ class CategoryDetailedSerializer < BasicCategorySerializer is_uncategorized end - def include_displayable_topics? - displayable_topics.present? - end - def description_excerpt - PrettyText.excerpt(description,300) if description + PrettyText.excerpt(description, 300) if description end def include_subcategory_ids? subcategory_ids.present? end - # Topic and post counts, including counts from the sub-categories: - def topics_day count_with_subcategories(:topics_day) end @@ -55,22 +42,6 @@ class CategoryDetailedSerializer < BasicCategorySerializer count_with_subcategories(:topics_year) end - def posts_day - count_with_subcategories(:posts_day) - end - - def posts_week - count_with_subcategories(:posts_week) - end - - def posts_month - count_with_subcategories(:posts_month) - end - - def posts_year - count_with_subcategories(:posts_year) - end - def count_with_subcategories(method) count = object.send(method) || 0 object.subcategories.each do |category| diff --git a/app/views/categories/index.html.erb b/app/views/categories/index.html.erb index f3ef3b7d6a0..2d2b32d3e59 100644 --- a/app/views/categories/index.html.erb +++ b/app/views/categories/index.html.erb @@ -1,24 +1,12 @@
- <% @list.categories.each do |c| %> + <% @category_list.categories.each do |c| %>

<%= c.name %>

<%= c.description %> -
- <%- if c.displayable_topics.present? %> - <% c.displayable_topics.each do |t| %> -
- - - <%= t.title %> - '>(<%= t.posts_count %>) -
- <% end %> - <%- end %> -
<% end %>
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 727f005546f..2dbc72f55e4 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -458,14 +458,12 @@ en: latest_by: "latest by" toggle_ordering: "toggle ordering control" subcategories: "Subcategories" - topic_stats: "The number of new topics." + topic_sentence: + one: "1 topic" + other: "%{count} topics" topic_stat_sentence: one: "%{count} new topic in the past %{unit}." other: "%{count} new topics in the past %{unit}." - post_stats: "The number of new posts." - post_stat_sentence: - one: "%{count} new post in the past %{unit}." - other: "%{count} new posts in the past %{unit}." ip_lookup: title: IP Address Lookup @@ -1941,6 +1939,10 @@ en: posts: "Posts" posts_long: "there are {{number}} posts in this topic" + posts_likes: + one: "This topic has 1 reply." + other: "This topic has {{count}} replies." + # keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details posts_likes_MF: | This topic has {count, plural, one {1 reply} other {# replies}} {ratio, select, diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 3df6dcf634c..67fbc70f1ff 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1055,6 +1055,7 @@ en: alert_admins_if_errors_per_minute: "Number of errors per minute in order to trigger an admin alert. A value of 0 disables this feature. NOTE: requires restart." alert_admins_if_errors_per_hour: "Number of errors per hour in order to trigger an admin alert. A value of 0 disables this feature. NOTE: requires restart." + categories_topics: "Number of topics to show in /categories page." suggested_topics: "Number of suggested topics shown at the bottom of a topic." limit_suggested_to_category: "Only show topics from the current category in suggested topics." suggested_topics_max_days_old: "Suggested topics should not be more than n days old." diff --git a/config/site_settings.yml b/config/site_settings.yml index f1f7707c58e..2908c8d28b0 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -71,6 +71,9 @@ basic: set_locale_from_accept_language_header: default: false validator: "AllowUserLocaleEnabledValidator" + categories_topics: + default: 20 + min: 5 suggested_topics: client: true default: 5 diff --git a/lib/topic_query.rb b/lib/topic_query.rb index bdae2033b8b..7189c9e51cb 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -297,7 +297,6 @@ class TopicQuery end topics.each do |t| - t.allowed_user_ids = filter == :private_messages ? t.allowed_users.map{|u| u.id} : [] end diff --git a/spec/models/category_list_spec.rb b/spec/models/category_list_spec.rb index bb4f3d2d83c..e047945f222 100644 --- a/spec/models/category_list_spec.rb +++ b/spec/models/category_list_spec.rb @@ -19,32 +19,6 @@ describe CategoryList do expect(CategoryList.new(Guardian.new user).categories.count).to eq(1) expect(CategoryList.new(Guardian.new nil).categories.count).to eq(1) end - - it "doesn't show topics that you can't view" do - public_cat = Fabricate(:category) # public category - Fabricate(:topic, category: public_cat) - - private_cat = Fabricate(:category) # private category - Fabricate(:topic, category: private_cat) - private_cat.set_permissions(admins: :full) - private_cat.save - - secret_subcat = Fabricate(:category, parent_category_id: public_cat.id) # private subcategory - Fabricate(:topic, category: secret_subcat) - secret_subcat.set_permissions(admins: :full) - secret_subcat.save - - CategoryFeaturedTopic.feature_topics - - expect(CategoryList.new(Guardian.new(admin)).categories.find { |x| x.name == public_cat.name }.displayable_topics.count).to eq(2) - expect(CategoryList.new(Guardian.new(admin)).categories.find { |x| x.name == private_cat.name }.displayable_topics.count).to eq(1) - - expect(CategoryList.new(Guardian.new(user)).categories.find { |x| x.name == public_cat.name }.displayable_topics.count).to eq(1) - expect(CategoryList.new(Guardian.new(user)).categories.find { |x| x.name == private_cat.name }).to eq(nil) - - expect(CategoryList.new(Guardian.new(nil)).categories.find { |x| x.name == public_cat.name }.displayable_topics.count).to eq(1) - expect(CategoryList.new(Guardian.new(nil)).categories.find { |x| x.name == private_cat.name }).to eq(nil) - end end context "with a category" do @@ -63,27 +37,6 @@ describe CategoryList do end - context "with pinned topics in a category" do - let!(:topic1) { Fabricate(:topic, category: topic_category, bumped_at: 8.minutes.ago) } - let!(:topic2) { Fabricate(:topic, category: topic_category, bumped_at: 5.minutes.ago) } - let!(:topic3) { Fabricate(:topic, category: topic_category, bumped_at: 2.minutes.ago) } - let!(:pinned) { Fabricate(:topic, category: topic_category, pinned_at: 10.minutes.ago, bumped_at: 10.minutes.ago) } - let(:category) { category_list.categories.find{|c| c.id == topic_category.id} } - - before do - SiteSetting.stubs(:category_featured_topics).returns(2) - end - - it "returns pinned topic first" do - expect(category.displayable_topics.map(&:id)).to eq([pinned.id, topic3.id]) - end - - it "returns topics in bumped_at order if pinned was unpinned" do - PinnedCheck.stubs(:unpinned?).returns(true) - expect(category.displayable_topics.map(&:id)).to eq([topic3.id, topic2.id]) - end - end - end describe 'category order' do diff --git a/spec/serializers/category_detailed_serializer_spec.rb b/spec/serializers/category_detailed_serializer_spec.rb index cf76aa4c14a..e4e3b1bcc8a 100644 --- a/spec/serializers/category_detailed_serializer_spec.rb +++ b/spec/serializers/category_detailed_serializer_spec.rb @@ -5,26 +5,20 @@ describe CategoryDetailedSerializer do describe "counts" do it "works for categories with no subcategories" do - no_subcats = Fabricate(:category, topics_year: 10, topics_month: 5, topics_day: 2, posts_year: 13, posts_month: 7, posts_day: 3) + no_subcats = Fabricate(:category, topics_year: 10, topics_month: 5, topics_day: 2) json = CategoryDetailedSerializer.new(no_subcats, scope: Guardian.new, root: false).as_json expect(json[:topics_year]).to eq(10) expect(json[:topics_month]).to eq(5) expect(json[:topics_day]).to eq(2) - expect(json[:posts_year]).to eq(13) - expect(json[:posts_month]).to eq(7) - expect(json[:posts_day]).to eq(3) end it "includes counts from subcategories" do - parent = Fabricate(:category, topics_year: 10, topics_month: 5, topics_day: 2, posts_year: 13, posts_month: 7, posts_day: 3) - subcategory = Fabricate(:category, parent_category_id: parent.id, topics_year: 1, topics_month: 1, topics_day: 1, posts_year: 1, posts_month: 1, posts_day: 1) + parent = Fabricate(:category, topics_year: 10, topics_month: 5, topics_day: 2) + subcategory = Fabricate(:category, parent_category_id: parent.id, topics_year: 1, topics_month: 1, topics_day: 1) json = CategoryDetailedSerializer.new(parent, scope: Guardian.new, root: false).as_json expect(json[:topics_year]).to eq(11) expect(json[:topics_month]).to eq(6) expect(json[:topics_day]).to eq(3) - expect(json[:posts_year]).to eq(14) - expect(json[:posts_month]).to eq(8) - expect(json[:posts_day]).to eq(4) end end diff --git a/test/javascripts/models/category-test.js.es6 b/test/javascripts/models/category-test.js.es6 index cb6c9858e2c..ca338686eaa 100644 --- a/test/javascripts/models/category-test.js.es6 +++ b/test/javascripts/models/category-test.js.es6 @@ -88,44 +88,6 @@ test('findByIds', function() { deepEqual(Discourse.Category.findByIds([1,2,3]), _.values(categories)); }); -test('postCountStats', function() { - const store = createStore(); - const category1 = store.createRecord('category', {id: 1, slug: 'unloved', posts_year: 2, posts_month: 0, posts_week: 0, posts_day: 0}), - category2 = store.createRecord('category', {id: 2, slug: 'hasbeen', posts_year: 50, posts_month: 4, posts_week: 0, posts_day: 0}), - category3 = store.createRecord('category', {id: 3, slug: 'solastweek', posts_year: 250, posts_month: 200, posts_week: 50, posts_day: 0}), - category4 = store.createRecord('category', {id: 4, slug: 'hotstuff', posts_year: 500, posts_month: 280, posts_week: 100, posts_day: 22}), - category5 = store.createRecord('category', {id: 6, slug: 'empty', posts_year: 0, posts_month: 0, posts_week: 0, posts_day: 0}); - - let result = category1.get('postCountStats'); - equal(result.length, 1, "should only show year"); - equal(result[0].value, 2); - equal(result[0].unit, 'year'); - - result = category2.get('postCountStats'); - equal(result.length, 2, "should show month and year"); - equal(result[0].value, 4); - equal(result[0].unit, 'month'); - equal(result[1].value, 50); - equal(result[1].unit, 'year'); - - result = category3.get('postCountStats'); - equal(result.length, 2, "should show week and month"); - equal(result[0].value, 50); - equal(result[0].unit, 'week'); - equal(result[1].value, 200); - equal(result[1].unit, 'month'); - - result = category4.get('postCountStats'); - equal(result.length, 2, "should show day and week"); - equal(result[0].value, 22); - equal(result[0].unit, 'day'); - equal(result[1].value, 100); - equal(result[1].unit, 'week'); - - result = category5.get('postCountStats'); - equal(result.length, 0, "should show nothing"); -}); - test('search with category name', () => { const store = createStore(), category1 = store.createRecord('category', { id: 1, name: 'middle term', slug: 'different-slug' }),