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"}}
-
+
{{i18n 'categories.category'}} |
- {{i18n 'categories.latest'}} |
{{i18n 'categories.topics'}} |
@@ -18,7 +17,6 @@
{{#if c.logo_url}}
{{category-logo-link category=c}}
{{/if}}
-
{{{c.description_excerpt}}}
@@ -33,27 +31,66 @@
{{/if}}
-
- {{#each c.featuredTopics as |f|}}
- {{featured-topic topic=f latestTopicOnly=latestTopicOnly action="showTopicEntrance"}}
- {{/each}}
- |
-
-
-
- {{#each c.topicCountStats as |s|}}
-
- {{s.value}} |
- / {{s.unit}} |
-
- {{/each}}
-
-
+ |
+ {{{c.stat}}}
|
{{/each}}
{{/discovery-categories}}
-
{{/if}}
+
+
+
+ {{i18n "filters.latest.title"}} |
+
+
+
+ {{#each model.topicList.topics as |t|}}
+
+
+
+
+
+ {{#with t.posters.lastObject.user as |lastPoster|}}
+ {{#user-link user=lastPoster}}
+ {{avatar lastPoster imageSize="large"}}
+ {{/user-link}}
+ {{/with}}
+ |
+
+ |
+ {{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}}
+
+
+
+
+
+ |
+
+
+
+
+ {{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.description %>
-
- <%- if c.displayable_topics.present? %>
- <% c.displayable_topics.each do |t| %>
-
- <% 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' }),