UX: new /categories layout
This commit is contained in:
parent
36f0bd36f4
commit
6d1d7b7c8f
22 changed files with 273 additions and 446 deletions
app
assets
javascripts/discourse
helpers
models
routes
templates/discovery
stylesheets
controllers
models
serializers
views/categories
config
lib
spec
test/javascripts/models
|
@ -24,7 +24,7 @@ registerUnbound('number', (orig, params) => {
|
||||||
|
|
||||||
// Round off the thousands to one decimal place
|
// Round off the thousands to one decimal place
|
||||||
const n = number(orig);
|
const n = number(orig);
|
||||||
if (n !== title) {
|
if (n.toString() !== title.toString() && !params.noTitle) {
|
||||||
result += " title='" + Handlebars.Utils.escapeExpression(title) + "'";
|
result += " title='" + Handlebars.Utils.escapeExpression(title) + "'";
|
||||||
}
|
}
|
||||||
result += ">" + n + "</span>";
|
result += ">" + n + "</span>";
|
||||||
|
|
|
@ -14,6 +14,17 @@ CategoryList.reopenClass({
|
||||||
const users = Discourse.Model.extractByKey(result.featured_users, Discourse.User);
|
const users = Discourse.Model.extractByKey(result.featured_users, Discourse.User);
|
||||||
const list = Discourse.Category.list();
|
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 => {
|
result.category_list.categories.forEach(c => {
|
||||||
if (c.parent_category_id) {
|
if (c.parent_category_id) {
|
||||||
c.parentCategory = list.findBy('id', 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));
|
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 = `<span class="value">${stat}</span> / <span class="unit">${unit}</span>`;
|
||||||
|
c.statTitle = I18n.t("categories.topic_stat_sentence", { count: stat, unit: unit });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
c.stat = `<span class="value">${c.topic_count}</span>`;
|
||||||
|
c.statTitle = I18n.t("categories.topic_sentence", { count: c.topic_count });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
categories.pushObject(store.createRecord('category', c));
|
categories.pushObject(store.createRecord('category', c));
|
||||||
});
|
});
|
||||||
return categories;
|
return categories;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ajax } from 'discourse/lib/ajax';
|
import { ajax } from 'discourse/lib/ajax';
|
||||||
import RestModel from 'discourse/models/rest';
|
import RestModel from 'discourse/models/rest';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
import { on } from 'ember-addons/ember-computed-decorators';
|
import { on } from 'ember-addons/ember-computed-decorators';
|
||||||
import PermissionType from 'discourse/models/permission-type';
|
import PermissionType from 'discourse/models/permission-type';
|
||||||
|
|
||||||
|
@ -17,56 +18,64 @@ const Category = RestModel.extend({
|
||||||
availableGroups.removeObject(elem.group_name);
|
availableGroups.removeObject(elem.group_name);
|
||||||
return {
|
return {
|
||||||
group_name: elem.group_name,
|
group_name: elem.group_name,
|
||||||
permission: PermissionType.create({id: elem.permission_type})
|
permission: PermissionType.create({ id: elem.permission_type })
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
availablePermissions: function(){
|
@computed
|
||||||
return [ PermissionType.create({id: PermissionType.FULL}),
|
availablePermissions() {
|
||||||
PermissionType.create({id: PermissionType.CREATE_POST}),
|
return [
|
||||||
PermissionType.create({id: PermissionType.READONLY})
|
PermissionType.create({ id: PermissionType.FULL }),
|
||||||
];
|
PermissionType.create({ id: PermissionType.CREATE_POST }),
|
||||||
}.property(),
|
PermissionType.create({ id: PermissionType.READONLY })
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
searchContext: function() {
|
@computed("id")
|
||||||
return ({ type: 'category', id: this.get('id'), category: this });
|
searchContext(id) {
|
||||||
}.property('id'),
|
return { type: 'category', id, category: this };
|
||||||
|
},
|
||||||
|
|
||||||
url: function() {
|
@computed("name")
|
||||||
|
url() {
|
||||||
return Discourse.getURL("/c/") + Category.slugFor(this);
|
return Discourse.getURL("/c/") + Category.slugFor(this);
|
||||||
}.property('name'),
|
},
|
||||||
|
|
||||||
fullSlug: function() {
|
@computed("url")
|
||||||
return this.get("url").slice(3).replace("/", "-");
|
fullSlug(url) {
|
||||||
}.property("url"),
|
return url.slice(3).replace("/", "-");
|
||||||
|
},
|
||||||
|
|
||||||
nameLower: function() {
|
@computed("name")
|
||||||
return this.get('name').toLowerCase();
|
nameLower(name) {
|
||||||
}.property('name'),
|
return name.toLowerCase();
|
||||||
|
},
|
||||||
|
|
||||||
unreadUrl: function() {
|
@computed("url")
|
||||||
return this.get('url') + '/l/unread';
|
unreadUrl(url) {
|
||||||
}.property('url'),
|
return `${url}/l/unread`;
|
||||||
|
},
|
||||||
|
|
||||||
newUrl: function() {
|
@computed("url")
|
||||||
return this.get('url') + '/l/new';
|
newUrl(url) {
|
||||||
}.property('url'),
|
return `${url}/l/new`;
|
||||||
|
},
|
||||||
|
|
||||||
style: function() {
|
@computed("color", "text_color")
|
||||||
return "background-color: #" + this.get('category.color') + "; color: #" + this.get('category.text_color') + ";";
|
style(color, textColor) {
|
||||||
}.property('color', 'text_color'),
|
return `background-color: #${color}; color: #${textColor}`;
|
||||||
|
},
|
||||||
|
|
||||||
moreTopics: function() {
|
@computed("topic_count")
|
||||||
return this.get('topic_count') > Discourse.SiteSettings.category_featured_topics;
|
moreTopics(topicCount) {
|
||||||
}.property('topic_count'),
|
return topicCount > Discourse.SiteSettings.category_featured_topics;
|
||||||
|
},
|
||||||
|
|
||||||
save: function() {
|
save() {
|
||||||
var url = "/categories";
|
const id = this.get("id");
|
||||||
if (this.get('id')) {
|
const url = id ? `/categories/${id}` : "/categories";
|
||||||
url = "/categories/" + this.get('id');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ajax(url, {
|
return ajax(url, {
|
||||||
data: {
|
data: {
|
||||||
|
@ -91,111 +100,74 @@ const Category = RestModel.extend({
|
||||||
allowed_tags: this.get('allowed_tags'),
|
allowed_tags: this.get('allowed_tags'),
|
||||||
allowed_tag_groups: this.get('allowed_tag_groups')
|
allowed_tag_groups: this.get('allowed_tag_groups')
|
||||||
},
|
},
|
||||||
type: this.get('id') ? 'PUT' : 'POST'
|
type: id ? 'PUT' : 'POST'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
permissionsForUpdate: function(){
|
@computed("permissions")
|
||||||
var rval = {};
|
permissionsForUpdate(permissions) {
|
||||||
_.each(this.get("permissions"),function(p){
|
let rval = {};
|
||||||
rval[p.group_name] = p.permission.id;
|
permissions.forEach(p => rval[p.group_name] = p.permission.id);
|
||||||
});
|
|
||||||
return rval;
|
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("permissions").addObject(permission);
|
||||||
this.get("availableGroups").removeObject(permission.group_name);
|
this.get("availableGroups").removeObject(permission.group_name);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removePermission(permission) {
|
||||||
removePermission: function(permission){
|
|
||||||
this.get("permissions").removeObject(permission);
|
this.get("permissions").removeObject(permission);
|
||||||
this.get("availableGroups").addObject(permission.group_name);
|
this.get("availableGroups").addObject(permission.group_name);
|
||||||
},
|
},
|
||||||
|
|
||||||
permissions: function(){
|
@computed
|
||||||
|
permissions() {
|
||||||
return Em.A([
|
return Em.A([
|
||||||
{group_name: "everyone", permission: PermissionType.create({id: 1})},
|
{ group_name: "everyone", permission: PermissionType.create({id: 1}) },
|
||||||
{group_name: "admins", permission: PermissionType.create({id: 2}) },
|
{ group_name: "admins", permission: PermissionType.create({id: 2}) },
|
||||||
{group_name: "crap", permission: PermissionType.create({id: 3}) }
|
{ group_name: "crap", permission: PermissionType.create({id: 3}) }
|
||||||
]);
|
]);
|
||||||
}.property(),
|
},
|
||||||
|
|
||||||
latestTopic: function(){
|
@computed("topics")
|
||||||
var topics = this.get('topics');
|
latestTopic(topics) {
|
||||||
if (topics && topics.length) {
|
if (topics && topics.length) {
|
||||||
return topics[0];
|
return topics[0];
|
||||||
}
|
}
|
||||||
}.property("topics"),
|
},
|
||||||
|
|
||||||
featuredTopics: function() {
|
@computed("topics")
|
||||||
var topics = this.get('topics');
|
featuredTopics(topics) {
|
||||||
if (topics && topics.length) {
|
if (topics && topics.length) {
|
||||||
return topics.slice(0, Discourse.SiteSettings.category_featured_topics || 2);
|
return topics.slice(0, Discourse.SiteSettings.category_featured_topics || 2);
|
||||||
}
|
}
|
||||||
}.property('topics'),
|
},
|
||||||
|
|
||||||
unreadTopics: function() {
|
@computed("id", "topicTrackingState.messageCount")
|
||||||
return this.topicTrackingState.countUnread(this.get('id'));
|
unreadTopics(id) {
|
||||||
}.property('topicTrackingState.messageCount'),
|
return this.topicTrackingState.countUnread(id);
|
||||||
|
},
|
||||||
|
|
||||||
newTopics: function() {
|
@computed("id", "topicTrackingState.messageCount")
|
||||||
return this.topicTrackingState.countNew(this.get('id'));
|
newTopics(id) {
|
||||||
}.property('topicTrackingState.messageCount'),
|
return this.topicTrackingState.countNew(id);
|
||||||
|
},
|
||||||
|
|
||||||
topicStatsTitle: function() {
|
setNotification(notification_level) {
|
||||||
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";
|
|
||||||
this.set('notification_level', notification_level);
|
this.set('notification_level', notification_level);
|
||||||
return ajax(url, {
|
const url = `/category/${this.get('id')}/notifications`;
|
||||||
data: {
|
return ajax(url, { data: { notification_level }, type: 'POST' });
|
||||||
notification_level: notification_level
|
|
||||||
},
|
|
||||||
type: 'POST'
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
postCountStats: function() {
|
@computed("id")
|
||||||
return this.countStats('posts');
|
isUncategorizedCategory(id) {
|
||||||
}.property('posts_year', 'posts_month', 'posts_week', 'posts_day'),
|
return id === Discourse.Site.currentProp("uncategorized_category_id");
|
||||||
|
}
|
||||||
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')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var _uncategorized;
|
var _uncategorized;
|
||||||
|
|
|
@ -71,6 +71,11 @@ const Topic = RestModel.extend({
|
||||||
I18n.t('last_post') + ": " + longDate(this.get('bumpedAt'));
|
I18n.t('last_post') + ": " + longDate(this.get('bumpedAt'));
|
||||||
}.property('bumpedAt'),
|
}.property('bumpedAt'),
|
||||||
|
|
||||||
|
@computed('replyCount')
|
||||||
|
replyTitle(count) {
|
||||||
|
return I18n.t("posts_likes", { count });
|
||||||
|
},
|
||||||
|
|
||||||
createdAt: function() {
|
createdAt: function() {
|
||||||
return new Date(this.get('created_at'));
|
return new Date(this.get('created_at'));
|
||||||
}.property('created_at'),
|
}.property('created_at'),
|
||||||
|
|
|
@ -2,7 +2,7 @@ import showModal from "discourse/lib/show-modal";
|
||||||
import OpenComposer from "discourse/mixins/open-composer";
|
import OpenComposer from "discourse/mixins/open-composer";
|
||||||
import CategoryList from "discourse/models/category-list";
|
import CategoryList from "discourse/models/category-list";
|
||||||
import { defaultHomepage } from 'discourse/lib/utilities';
|
import { defaultHomepage } from 'discourse/lib/utilities';
|
||||||
import PreloadStore from 'preload-store';
|
import TopicList from "discourse/models/topic-list";
|
||||||
|
|
||||||
const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||||
renderTemplate() {
|
renderTemplate() {
|
||||||
|
@ -15,10 +15,6 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||||
},
|
},
|
||||||
|
|
||||||
model() {
|
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 => {
|
return CategoryList.list(this.store, 'categories').then(list => {
|
||||||
const tracking = this.topicTrackingState;
|
const tracking = this.topicTrackingState;
|
||||||
if (tracking) {
|
if (tracking) {
|
||||||
|
@ -35,6 +31,8 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
|
TopicList.find("latest").then(result => model.set("topicList", result));
|
||||||
|
|
||||||
controller.set("model", model);
|
controller.set("model", model);
|
||||||
|
|
||||||
this.controllerFor("navigation/categories").setProperties({
|
this.controllerFor("navigation/categories").setProperties({
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
{{#if model.categories}}
|
{{#if model.categories}}
|
||||||
{{#discovery-categories refresh="refresh"}}
|
{{#discovery-categories refresh="refresh"}}
|
||||||
<table class='topic-list categories'>
|
<table class='categories topic-list'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class='category'>{{i18n 'categories.category'}}</th>
|
<th class='category'>{{i18n 'categories.category'}}</th>
|
||||||
<th class='latest'>{{i18n 'categories.latest'}}</th>
|
|
||||||
<th class='stats topics'>{{i18n 'categories.topics'}}</th>
|
<th class='stats topics'>{{i18n 'categories.topics'}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -18,7 +17,6 @@
|
||||||
{{#if c.logo_url}}
|
{{#if c.logo_url}}
|
||||||
{{category-logo-link category=c}}
|
{{category-logo-link category=c}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="category-description">
|
<div class="category-description">
|
||||||
{{{c.description_excerpt}}}
|
{{{c.description_excerpt}}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,27 +31,66 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
<td class="{{if c.archived 'archived'}} latest">
|
<td class='stats' title={{c.statTitle}}>
|
||||||
{{#each c.featuredTopics as |f|}}
|
{{{c.stat}}}
|
||||||
{{featured-topic topic=f latestTopicOnly=latestTopicOnly action="showTopicEntrance"}}
|
|
||||||
{{/each}}
|
|
||||||
</td>
|
|
||||||
<td class='stats' title={{c.topicStatsTitle}}>
|
|
||||||
<table class="categoryStats">
|
|
||||||
<tbody>
|
|
||||||
{{#each c.topicCountStats as |s|}}
|
|
||||||
<tr>
|
|
||||||
<td class="value">{{s.value}}</td>
|
|
||||||
<td class="unit"> / {{s.unit}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{{/discovery-categories}}
|
{{/discovery-categories}}
|
||||||
<footer class='topic-list-bottom'></footer>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
<table class="topic-list topic-list-latest">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class='category'>{{i18n "filters.latest.title"}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each model.topicList.topics as |t|}}
|
||||||
|
<tr>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr class="{{if t.archived 'archived'}}" data-topic-id={{unbound t.id}}>
|
||||||
|
<td class="topic-poster">
|
||||||
|
{{#with t.posters.lastObject.user as |lastPoster|}}
|
||||||
|
{{#user-link user=lastPoster}}
|
||||||
|
{{avatar lastPoster imageSize="large"}}
|
||||||
|
{{/user-link}}
|
||||||
|
{{/with}}
|
||||||
|
</td>
|
||||||
|
<td class="main-link">
|
||||||
|
<tr>
|
||||||
|
{{topic-status topic=t}}
|
||||||
|
{{topic-link t}}
|
||||||
|
{{#if t.unseen}}
|
||||||
|
<span class="badge-notification new-topic"></span>
|
||||||
|
{{/if}}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{{category-link t.category}}
|
||||||
|
{{#if t.tags}}
|
||||||
|
{{#each t.visibleListTags as |tag|}}
|
||||||
|
{{discourse-tag tag}}
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
</tr>
|
||||||
|
</td>
|
||||||
|
<td class="topic-stats">
|
||||||
|
<div class="topic-replies">
|
||||||
|
<a href="{{t.lastPostUrl}}" title="{{t.replyTitle}}">{{number t.replyCount noTitle="true"}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="topic-last-activity">
|
||||||
|
<a href="{{t.lastPostUrl}}" title="{{t.bumpedAtTitle}}">{{format-date t.bumpedAt format="tiny" noTitle="true"}}</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</tr>
|
||||||
|
{{else}}
|
||||||
|
{{loading-spinner}}
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
|
|
@ -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 {
|
.topic-list.categories {
|
||||||
|
|
||||||
|
|
|
@ -169,6 +169,7 @@ header .discourse-tag {color: $tag-color !important; }
|
||||||
|
|
||||||
.bullet + .list-tags {
|
.bullet + .list-tags {
|
||||||
display: block;
|
display: block;
|
||||||
|
line-height: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar + .list-tags {
|
.bar + .list-tags {
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 15px;
|
||||||
|
|
||||||
span.badge-category {
|
span.badge-category {
|
||||||
color: $primary !important;
|
color: $primary !important;
|
||||||
|
|
|
@ -193,8 +193,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.category{
|
.category{
|
||||||
width: 45%;
|
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 1.286em;
|
font-size: 1.286em;
|
||||||
|
|
|
@ -12,28 +12,29 @@ class CategoriesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def index
|
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
|
discourse_expires_in 1.minute
|
||||||
|
|
||||||
unless current_homepage == "categories"
|
@description = SiteSetting.site_description
|
||||||
@title = I18n.t('js.filters.categories.title')
|
|
||||||
end
|
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|
|
respond_to do |format|
|
||||||
format.html { render }
|
format.html do
|
||||||
format.json { render_serialized(@list, CategoryListSerializer) }
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ class Category < ActiveRecord::Base
|
||||||
|
|
||||||
# permission is just used by serialization
|
# permission is just used by serialization
|
||||||
# we may consider wrapping this in another spot
|
# 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')
|
@topic_id_cache = DistributedCache.new('category_topic_ids')
|
||||||
|
|
||||||
|
@ -187,9 +187,7 @@ SQL
|
||||||
self.topic_id ? query.where(['topics.id <> ?', self.topic_id]) : query
|
self.topic_id ? query.where(['topics.id <> ?', self.topic_id]) : query
|
||||||
end
|
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
|
def self.post_template
|
||||||
I18n.t("category.post_template", replace_paragraph: I18n.t("category.replace_paragraph"))
|
I18n.t("category.post_template", replace_paragraph: I18n.t("category.replace_paragraph"))
|
||||||
end
|
end
|
||||||
|
@ -219,7 +217,6 @@ SQL
|
||||||
@@cache.getset(self.description) do
|
@@cache.getset(self.description) do
|
||||||
Nokogiri::HTML(self.description).text
|
Nokogiri::HTML(self.description).text
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def duplicate_slug?
|
def duplicate_slug?
|
||||||
|
|
|
@ -1,75 +1,37 @@
|
||||||
require_dependency 'pinned_check'
|
|
||||||
|
|
||||||
class CategoryList
|
class CategoryList
|
||||||
include ActiveModel::Serialization
|
include ActiveModel::Serialization
|
||||||
|
|
||||||
attr_accessor :categories,
|
attr_accessor :categories,
|
||||||
:topic_users,
|
|
||||||
:uncategorized,
|
:uncategorized,
|
||||||
:draft,
|
:draft,
|
||||||
:draft_key,
|
:draft_key,
|
||||||
:draft_sequence
|
:draft_sequence
|
||||||
|
|
||||||
def initialize(guardian=nil, options = {})
|
def initialize(guardian=nil, options={})
|
||||||
@guardian = guardian || Guardian.new
|
@guardian = guardian || Guardian.new
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
find_relevant_topics unless latest_post_only?
|
|
||||||
find_categories
|
find_categories
|
||||||
|
end
|
||||||
|
|
||||||
prune_empty
|
def preload_key
|
||||||
find_user_data
|
"categories_list".freeze
|
||||||
sort_unpinned
|
|
||||||
trim_results
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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
|
# Find a list of all categories to associate the topics with
|
||||||
def find_categories
|
def find_categories
|
||||||
@categories = Category
|
@categories = Category.includes(:topic_only_relative_url, subcategories: [:topic_only_relative_url]).secured(@guardian)
|
||||||
.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 = @categories.where(suppress_from_homepage: false) if @options[:is_homepage]
|
@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
|
if SiteSetting.fixed_category_positions
|
||||||
@categories = @categories.order('position ASC').order('id ASC')
|
@categories = @categories.order(:position, :id)
|
||||||
else
|
else
|
||||||
@categories = @categories.order('COALESCE(categories.posts_week, 0) DESC')
|
@categories = @categories.order('COALESCE(categories.posts_week, 0) DESC')
|
||||||
.order('COALESCE(categories.posts_month, 0) DESC')
|
.order('COALESCE(categories.posts_month, 0) DESC')
|
||||||
|
@ -77,10 +39,6 @@ class CategoryList
|
||||||
.order('id ASC')
|
.order('id ASC')
|
||||||
end
|
end
|
||||||
|
|
||||||
if latest_post_only?
|
|
||||||
@categories = @categories.includes(latest_post: { topic: :last_poster })
|
|
||||||
end
|
|
||||||
|
|
||||||
@categories = @categories.to_a
|
@categories = @categories.to_a
|
||||||
|
|
||||||
category_user = {}
|
category_user = {}
|
||||||
|
@ -95,95 +53,18 @@ class CategoryList
|
||||||
category.has_children = category.subcategories.present?
|
category.has_children = category.subcategories.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
if @options[:parent_category_id].blank?
|
subcategories = {}
|
||||||
subcategories = {}
|
to_delete = Set.new
|
||||||
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
|
|
||||||
@categories.each do |c|
|
@categories.each do |c|
|
||||||
next if c.displayable_topics.blank?
|
if c.parent_category_id.present?
|
||||||
c.displayable_topics = c.displayable_topics[0,latest_posts_count]
|
subcategories[c.parent_category_id] ||= []
|
||||||
|
subcategories[c.parent_category_id] << c.id
|
||||||
|
to_delete << c
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@categories.each { |c| c.subcategory_ids = subcategories[c.id] }
|
||||||
|
|
||||||
|
@categories.delete_if { |c| to_delete.include?(c) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,17 +6,10 @@ class CategoryDetailedSerializer < BasicCategorySerializer
|
||||||
:topics_week,
|
:topics_week,
|
||||||
:topics_month,
|
:topics_month,
|
||||||
:topics_year,
|
:topics_year,
|
||||||
:posts_day,
|
|
||||||
:posts_week,
|
|
||||||
:posts_month,
|
|
||||||
:posts_year,
|
|
||||||
:description_excerpt,
|
:description_excerpt,
|
||||||
:is_uncategorized,
|
:is_uncategorized,
|
||||||
:subcategory_ids
|
:subcategory_ids
|
||||||
|
|
||||||
has_many :featured_users, serializer: BasicUserSerializer
|
|
||||||
has_many :displayable_topics, serializer: ListableTopicSerializer, embed: :objects, key: :topics
|
|
||||||
|
|
||||||
def is_uncategorized
|
def is_uncategorized
|
||||||
object.id == SiteSetting.uncategorized_category_id
|
object.id == SiteSetting.uncategorized_category_id
|
||||||
end
|
end
|
||||||
|
@ -25,20 +18,14 @@ class CategoryDetailedSerializer < BasicCategorySerializer
|
||||||
is_uncategorized
|
is_uncategorized
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_displayable_topics?
|
|
||||||
displayable_topics.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def description_excerpt
|
def description_excerpt
|
||||||
PrettyText.excerpt(description,300) if description
|
PrettyText.excerpt(description, 300) if description
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_subcategory_ids?
|
def include_subcategory_ids?
|
||||||
subcategory_ids.present?
|
subcategory_ids.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Topic and post counts, including counts from the sub-categories:
|
|
||||||
|
|
||||||
def topics_day
|
def topics_day
|
||||||
count_with_subcategories(:topics_day)
|
count_with_subcategories(:topics_day)
|
||||||
end
|
end
|
||||||
|
@ -55,22 +42,6 @@ class CategoryDetailedSerializer < BasicCategorySerializer
|
||||||
count_with_subcategories(:topics_year)
|
count_with_subcategories(:topics_year)
|
||||||
end
|
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)
|
def count_with_subcategories(method)
|
||||||
count = object.send(method) || 0
|
count = object.send(method) || 0
|
||||||
object.subcategories.each do |category|
|
object.subcategories.each do |category|
|
||||||
|
|
|
@ -1,24 +1,12 @@
|
||||||
<div class='category-list' itemscope itemtype='http://schema.org/ItemList'>
|
<div class='category-list' itemscope itemtype='http://schema.org/ItemList'>
|
||||||
<meta itemprop='itemListOrder' content='http://schema.org/ItemListOrderDescending'>
|
<meta itemprop='itemListOrder' content='http://schema.org/ItemListOrderDescending'>
|
||||||
<% @list.categories.each do |c| %>
|
<% @category_list.categories.each do |c| %>
|
||||||
<div class='category' itemprop='itemListElement' itemscope itemtype='http://schema.org/ListItem'>
|
<div class='category' itemprop='itemListElement' itemscope itemtype='http://schema.org/ListItem'>
|
||||||
<meta itemprop='url' content='<%= c.url %>'>
|
<meta itemprop='url' content='<%= c.url %>'>
|
||||||
<h2><a href='<%= c.url %>' itemprop='item'>
|
<h2><a href='<%= c.url %>' itemprop='item'>
|
||||||
<span itemprop='name'><%= c.name %></span>
|
<span itemprop='name'><%= c.name %></span>
|
||||||
</a></h2>
|
</a></h2>
|
||||||
<span itemprop='description'><%= c.description %></span>
|
<span itemprop='description'><%= c.description %></span>
|
||||||
<div class='topic-list' itemscope itemtype='http://schema.org/ItemList'>
|
|
||||||
<%- if c.displayable_topics.present? %>
|
|
||||||
<% c.displayable_topics.each do |t| %>
|
|
||||||
<div itemprop='itemListElement' itemscope itemtype='http://schema.org/ListItem'>
|
|
||||||
<meta itemprop='url' content='<%= t.url %>'>
|
|
||||||
<a href='<%= t.relative_url %>' itemprop='item'>
|
|
||||||
<span itemprop='name'><%= t.title %></span>
|
|
||||||
</a> <span title='<%= t 'posts' %>'>(<%= t.posts_count %>)</span>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%- end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -458,14 +458,12 @@ en:
|
||||||
latest_by: "latest by"
|
latest_by: "latest by"
|
||||||
toggle_ordering: "toggle ordering control"
|
toggle_ordering: "toggle ordering control"
|
||||||
subcategories: "Subcategories"
|
subcategories: "Subcategories"
|
||||||
topic_stats: "The number of new topics."
|
topic_sentence:
|
||||||
|
one: "1 topic"
|
||||||
|
other: "%{count} topics"
|
||||||
topic_stat_sentence:
|
topic_stat_sentence:
|
||||||
one: "%{count} new topic in the past %{unit}."
|
one: "%{count} new topic in the past %{unit}."
|
||||||
other: "%{count} new topics 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:
|
ip_lookup:
|
||||||
title: IP Address Lookup
|
title: IP Address Lookup
|
||||||
|
@ -1941,6 +1939,10 @@ en:
|
||||||
posts: "Posts"
|
posts: "Posts"
|
||||||
posts_long: "there are {{number}} posts in this topic"
|
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
|
# keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details
|
||||||
posts_likes_MF: |
|
posts_likes_MF: |
|
||||||
This topic has {count, plural, one {1 reply} other {# replies}} {ratio, select,
|
This topic has {count, plural, one {1 reply} other {# replies}} {ratio, select,
|
||||||
|
|
|
@ -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_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."
|
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."
|
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."
|
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."
|
suggested_topics_max_days_old: "Suggested topics should not be more than n days old."
|
||||||
|
|
|
@ -71,6 +71,9 @@ basic:
|
||||||
set_locale_from_accept_language_header:
|
set_locale_from_accept_language_header:
|
||||||
default: false
|
default: false
|
||||||
validator: "AllowUserLocaleEnabledValidator"
|
validator: "AllowUserLocaleEnabledValidator"
|
||||||
|
categories_topics:
|
||||||
|
default: 20
|
||||||
|
min: 5
|
||||||
suggested_topics:
|
suggested_topics:
|
||||||
client: true
|
client: true
|
||||||
default: 5
|
default: 5
|
||||||
|
|
|
@ -297,7 +297,6 @@ class TopicQuery
|
||||||
end
|
end
|
||||||
|
|
||||||
topics.each do |t|
|
topics.each do |t|
|
||||||
|
|
||||||
t.allowed_user_ids = filter == :private_messages ? t.allowed_users.map{|u| u.id} : []
|
t.allowed_user_ids = filter == :private_messages ? t.allowed_users.map{|u| u.id} : []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -19,32 +19,6 @@ describe CategoryList do
|
||||||
expect(CategoryList.new(Guardian.new user).categories.count).to eq(1)
|
expect(CategoryList.new(Guardian.new user).categories.count).to eq(1)
|
||||||
expect(CategoryList.new(Guardian.new nil).categories.count).to eq(1)
|
expect(CategoryList.new(Guardian.new nil).categories.count).to eq(1)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context "with a category" do
|
context "with a category" do
|
||||||
|
@ -63,27 +37,6 @@ describe CategoryList do
|
||||||
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe 'category order' do
|
describe 'category order' do
|
||||||
|
|
|
@ -5,26 +5,20 @@ describe CategoryDetailedSerializer do
|
||||||
|
|
||||||
describe "counts" do
|
describe "counts" do
|
||||||
it "works for categories with no subcategories" 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
|
json = CategoryDetailedSerializer.new(no_subcats, scope: Guardian.new, root: false).as_json
|
||||||
expect(json[:topics_year]).to eq(10)
|
expect(json[:topics_year]).to eq(10)
|
||||||
expect(json[:topics_month]).to eq(5)
|
expect(json[:topics_month]).to eq(5)
|
||||||
expect(json[:topics_day]).to eq(2)
|
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
|
end
|
||||||
|
|
||||||
it "includes counts from subcategories" do
|
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)
|
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, posts_year: 1, posts_month: 1, posts_day: 1)
|
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
|
json = CategoryDetailedSerializer.new(parent, scope: Guardian.new, root: false).as_json
|
||||||
expect(json[:topics_year]).to eq(11)
|
expect(json[:topics_year]).to eq(11)
|
||||||
expect(json[:topics_month]).to eq(6)
|
expect(json[:topics_month]).to eq(6)
|
||||||
expect(json[:topics_day]).to eq(3)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -88,44 +88,6 @@ test('findByIds', function() {
|
||||||
deepEqual(Discourse.Category.findByIds([1,2,3]), _.values(categories));
|
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', () => {
|
test('search with category name', () => {
|
||||||
const store = createStore(),
|
const store = createStore(),
|
||||||
category1 = store.createRecord('category', { id: 1, name: 'middle term', slug: 'different-slug' }),
|
category1 = store.createRecord('category', { id: 1, name: 'middle term', slug: 'different-slug' }),
|
||||||
|
|
Reference in a new issue