Routes and support for sub-categories

This commit is contained in:
Robin Ward 2013-10-23 14:40:39 -04:00
parent 49a11e51df
commit 541620c115
15 changed files with 206 additions and 92 deletions

View File

@ -33,22 +33,26 @@ Discourse.Utilities = {
}
},
// Create a badge like category link
/**
Create a badge-like category link
@method categoryLink
@param {Discourse.Category} category the category whose link we want
@returns {String} the html category badge
**/
categoryLink: function(category) {
if (!category) return "";
var color = Em.get(category, 'color');
var textColor = Em.get(category, 'text_color');
var name = Em.get(category, 'name');
var description = Em.get(category, 'description');
// Build the HTML link
var result = "<a href=\"" + Discourse.getURL("/category/") + Discourse.Category.slugFor(category) + "\" class=\"badge-category\" ";
var color = Em.get(category, 'color'),
textColor = Em.get(category, 'text_color'),
name = Em.get(category, 'name'),
description = Em.get(category, 'description'),
html = "<a href=\"" + Discourse.getURL("/category/") + Discourse.Category.slugFor(category) + "\" class=\"badge-category\" ";
// Add description if we have it
if (description) result += "title=\"" + Handlebars.Utils.escapeExpression(description) + "\" ";
if (description) html += "title=\"" + Handlebars.Utils.escapeExpression(description) + "\" ";
return result + "style=\"background-color: #" + color + "; color: #" + textColor + ";\">" + name + "</a>";
return html + "style=\"background-color: #" + color + "; color: #" + textColor + ";\">" + name + "</a>";
},
avatarUrl: function(template, size) {

View File

@ -132,18 +132,56 @@ Discourse.Category.reopenClass({
slugFor: function(category) {
if (!category) return "";
var id = Em.get(category, 'id');
var slug = Em.get(category, 'slug');
if (!slug || slug.trim().length === 0) return "" + id + "-category";
return slug;
var parentCategory = Em.get(category, 'parentCategory'),
result = "";
if (parentCategory) {
result = Discourse.Category.slugFor(parentCategory) + "/";
}
var id = Em.get(category, 'id'),
slug = Em.get(category, 'slug');
if (!slug || slug.trim().length === 0) return result + id + "-category";
return result + slug;
},
list: function() {
return Discourse.Site.currentProp('categories');
},
findBySlugOrId: function(slugOrId) {
// TODO: all our routing around categories need a rethink
findBySlug: function(slug, parentSlug) {
var uncategorized = Discourse.Category.uncategorizedInstance();
if (slug === uncategorized.get('slug')) return uncategorized;
var categories = Discourse.Category.list(),
category;
if (parentSlug) {
var parentCategory = categories.findBy('slug', parentSlug);
if (parentCategory) {
category = categories.find(function(item) {
return item && item.get('parentCategory') === parentCategory && item.get('slug') === slug;
});
}
} else {
category = categories.findBy('slug', slug);
// If we have a parent category, we need to enforce it
if (category.get('parentCategory')) return;
}
// In case the slug didn't work, try to find it by id instead.
if (!category) {
category = categories.findBy('id', parseInt(slug, 10));
}
return category;
},
reloadBySlugOrId: function(slugOrId) {
return Discourse.ajax("/category/" + slugOrId + "/show.json").then(function (result) {
return Discourse.Category.create(result.category);
});

View File

@ -24,18 +24,23 @@ Discourse.CategoryList = Ember.ArrayProxy.extend({
Discourse.CategoryList.reopenClass({
categoriesFrom: function(result) {
var categories = Discourse.CategoryList.create();
var users = Discourse.Model.extractByKey(result.featured_users, Discourse.User);
var categories = Discourse.CategoryList.create(),
users = Discourse.Model.extractByKey(result.featured_users, Discourse.User),
list = Discourse.Category.list();
result.category_list.categories.forEach(function(c) {
if (c.parent_category_id) {
c.parentCategory = list.findBy('id', c.parent_category_id);
}
_.each(result.category_list.categories,function(c) {
if (c.featured_user_ids) {
c.featured_users = _.map(c.featured_user_ids,function(u) {
c.featured_users = c.featured_user_ids.map(function(u) {
return users[u];
});
}
if (c.topics) {
c.topics = _.map(c.topics,function(t) {
c.topics = c.topics.map(function(t) {
return Discourse.Topic.create(t);
});
}
@ -58,8 +63,9 @@ Discourse.CategoryList.reopenClass({
},
list: function(filter) {
var self = this;
var finder = null;
var self = this,
finder = null;
if (filter === 'categories') {
finder = PreloadStore.getAndRemove("categories_list", function() {
return Discourse.ajax("/categories.json");

View File

@ -48,7 +48,7 @@ Discourse.Site.reopenClass(Discourse.Singleton, {
var result = this._super(obj);
if (result.categories) {
var byId = {}
var byId = {};
result.categories = _.map(result.categories, function(c) {
byId[c.id] = Discourse.Category.create(c);
return byId[c.id];
@ -59,7 +59,7 @@ Discourse.Site.reopenClass(Discourse.Singleton, {
if (c.get('parent_category_id')) {
c.set('parentCategory', byId[c.get('parent_category_id')]);
}
})
});
}
if (result.trust_levels) {

View File

@ -83,9 +83,9 @@ Discourse.TopicList = Discourse.Model.extend({
Discourse.TopicList.reopenClass({
loadTopics: function(topic_ids, filter) {
var defer = new Ember.Deferred();
var defer = new Ember.Deferred(),
url = Discourse.getURL("/") + filter + "?topic_ids=" + topic_ids.join(",");
var url = Discourse.getURL("/") + filter + "?topic_ids=" + topic_ids.join(",");
Discourse.ajax({url: url}).then(function (result) {
if (result) {
// the new topics loaded from the server
@ -107,37 +107,45 @@ Discourse.TopicList.reopenClass({
return defer;
},
/**
Stitch together side loaded topic data
@method topicsFrom
@param {Object} JSON object with topic data
@returns {Array} the list of topics
**/
topicsFrom: function(result) {
// Stitch together our side loaded data
var categories, topics, users;
categories = this.extractByKey(result.categories, Discourse.Category);
users = this.extractByKey(result.users, Discourse.User);
topics = Em.A();
var categories = Discourse.Category.list(),
users = this.extractByKey(result.users, Discourse.User),
topics = Em.A();
_.each(result.topic_list.topics,function(ft) {
ft.category = categories[ft.category_id];
_.each(ft.posters,function(p) {
return result.topic_list.topics.map(function (t) {
t.category = categories.findBy('id', t.category_id);
t.posters.forEach(function(p) {
p.user = users[p.user_id];
});
topics.pushObject(Discourse.Topic.create(ft));
return Discourse.Topic.create(t);
});
return topics;
},
/**
Lists topics on a given menu item
@method list
@param {Object} The menu item to filter to
@returns {Promise} a promise that resolves to the list of topics
**/
list: function(menuItem) {
var filter = menuItem.get('name');
var filter = menuItem.get('name'),
session = Discourse.Session.current(),
list = session.get('topicList');
var session = Discourse.Session.current();
var list = session.get('topicList');
if (list) {
if ((list.get('filter') === filter) && window.location.pathname.indexOf('more') > 0) {
list.set('loaded', true);
return Ember.RSVP.resolve(list);
}
if (list && (list.get('filter') === filter) && window.location.pathname.indexOf('more') > 0) {
list.set('loaded', true);
return Ember.RSVP.resolve(list);
}
session.set('topicList', null);
session.set('topicListScrollPos', null);
session.setProperties({topicList: null, topicListScrollPos: null});
return Discourse.TopicList.find(filter, menuItem.get('excludeCategory'));
}

View File

@ -68,7 +68,7 @@ Discourse.ApplicationRoute = Em.Route.extend({
Discourse.Route.showModal(router, 'editCategory', category);
router.controllerFor('editCategory').set('selectedTab', 'general');
} else {
Discourse.Category.findBySlugOrId(category.get('slug') || category.get('id')).then(function (c) {
Discourse.Category.reloadBySlugOrId(category.get('slug') || category.get('id')).then(function (c) {
Discourse.Site.current().updateCategory(c);
Discourse.Route.showModal(router, 'editCategory', c);
router.controllerFor('editCategory').set('selectedTab', 'general');

View File

@ -29,13 +29,14 @@ Discourse.Route.buildRoutes(function() {
});
// the homepage is the first item of the 'top_menu' site setting
var settings = Discourse.SiteSettings;
var homepage = settings.top_menu.split("|")[0].split(",")[0];
var homepage = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
this.route(homepage, { path: '/' });
this.route('categories', { path: '/categories' });
this.route('category', { path: '/category/:slug/more' });
this.route('category', { path: '/category/:slug' });
this.route('category', { path: '/category/:slug/more' });
this.route('category', { path: '/category/:parentSlug/:slug' });
this.route('category', { path: '/category/:parentSlug/:slug/more' });
});
// User routes

View File

@ -9,21 +9,7 @@
Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
model: function(params) {
var categories = Discourse.Category.list();
var slug = Em.get(params, 'slug');
var uncategorized = Discourse.Category.uncategorizedInstance();
if (slug === uncategorized.get('slug')) return uncategorized;
var category = categories.findProperty('slug', Em.get(params, 'slug'));
// In case the slug didn't work, try to find it by id instead.
if (!category) {
category = categories.findProperty('id', parseInt(slug, 10));
}
return category;
return Discourse.Category.findBySlug(Em.get(params, 'slug'), Em.get(params, 'parentSlug'));
},
setupController: function(controller, category) {
@ -35,16 +21,18 @@ Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
}
}
var listController = this.controllerFor('list');
var urlId = Discourse.Category.slugFor(category);
listController.set('filterMode', "category/" + urlId);
var listController = this.controllerFor('list'),
urlId = Discourse.Category.slugFor(category),
self = this;
var router = this;
listController.set('filterMode', "category/" + urlId);
listController.load("category/" + urlId).then(function(topicList) {
listController.set('canCreateTopic', topicList.get('can_create_topic'));
listController.set('category', category);
router.controllerFor('listTopics').set('content', topicList);
router.controllerFor('listTopics').set('category', category);
listController.setProperties({
canCreateTopic: topicList.get('can_create_topic'),
category: category
});
self.controllerFor('listTopics').set('content', topicList);
self.controllerFor('listTopics').set('category', category);
});
},

View File

@ -62,7 +62,12 @@ class ListController < ApplicationController
@description = @category.description
end
list.more_topics_url = url_for(category_list_path(params[:category], page: next_page, format: "json"))
if params[:parent_category].present?
list.more_topics_url = url_for(category_list_parent_path(params[:parent_category], params[:category], page: next_page, format: "json"))
else
list.more_topics_url = url_for(category_list_path(params[:category], page: next_page, format: "json"))
end
respond(list)
end
@ -118,7 +123,18 @@ class ListController < ApplicationController
def set_category
slug = params.fetch(:category)
@category = Category.where("slug = ?", slug).includes(:featured_users).first || Category.where("id = ?", slug.to_i).includes(:featured_users).first
parent_slug = params[:parent_category]
parent_category_id = nil
if parent_slug.present?
parent_category_id = Category.where(slug: parent_slug).pluck(:id).first ||
Category.where(id: parent_slug.to_i).pluck(:id).first
raise Discourse::NotFound.new if parent_category_id.blank?
end
@category = Category.where(slug: slug, parent_category_id: parent_category_id).includes(:featured_users).first ||
Category.where(id: slug.to_i, parent_category_id: parent_category_id).includes(:featured_users).first
end
def request_is_for_uncategorized?

View File

@ -13,4 +13,8 @@ class BasicCategorySerializer < ApplicationSerializer
:permission,
:parent_category_id
def include_parent_category_id?
parent_category_id
end
end

View File

@ -1,16 +1,9 @@
class CategoryDetailedSerializer < ApplicationSerializer
class CategoryDetailedSerializer < BasicCategorySerializer
attributes :id,
:name,
:color,
:text_color,
:slug,
:topic_count,
:post_count,
attributes :post_count,
:topics_week,
:topics_month,
:topics_year,
:description,
:description_excerpt,
:is_uncategorized

View File

@ -6,9 +6,9 @@ class TopicListItemSerializer < ListableTopicSerializer
:has_best_of,
:archetype,
:rank_details,
:last_poster_username
:last_poster_username,
:category_id
has_one :category, serializer: BasicCategorySerializer
has_many :posters, serializer: TopicPosterSerializer, embed: :objects
def starred

View File

@ -196,6 +196,7 @@ Discourse::Application.routes.draw do
get 'category/:category.rss' => 'list#category_feed', format: :rss, as: 'category_feed'
get 'category/:category' => 'list#category', as: 'category_list'
get 'category/:parent_category/:category' => 'list#category', as: 'category_list_parent'
get 'category/:category/more' => 'list#category', as: 'category_list_more'
# We've renamed popular to latest. If people access it we want a permanent redirect.

View File

@ -92,6 +92,35 @@ describe ListController do
end
end
context 'a child category' do
let(:sub_category) { Fabricate(:category, parent_category_id: category.id) }
context 'when parent and child are requested' do
before do
xhr :get, :category, parent_category: category.slug, category: sub_category.slug
end
it { should respond_with(:success) }
end
context 'when child is requested with the wrong parent' do
before do
xhr :get, :category, parent_category: 'not_the_right_slug', category: sub_category.slug
end
it { should_not respond_with(:success) }
end
context 'when child is requested without a parent' do
before do
xhr :get, :category, category: sub_category.slug
end
it { should_not respond_with(:success) }
end
end
describe 'feed' do
it 'renders RSS' do
get :category_feed, category: category.slug, format: :rss

View File

@ -2,13 +2,39 @@ module("Discourse.Category");
test('slugFor', function(){
var slugFor = function(args, val, text) {
equal(Discourse.Category.slugFor(args), val, text);
var slugFor = function(cat, val, text) {
equal(Discourse.Category.slugFor(cat), val, text);
};
slugFor({slug: 'hello'}, "hello", "It calculates the proper slug for hello");
slugFor({id: 123, slug: ''}, "123-category", "It returns id-category for empty strings");
slugFor({id: 456}, "456-category", "It returns id-category for undefined slugs");
slugFor(Discourse.Category.create({slug: 'hello'}), "hello", "It calculates the proper slug for hello");
slugFor(Discourse.Category.create({id: 123, slug: ''}), "123-category", "It returns id-category for empty strings");
slugFor(Discourse.Category.create({id: 456}), "456-category", "It returns id-category for undefined slugs");
var parentCategory = Discourse.Category.create({id: 345, slug: 'darth'});
slugFor(Discourse.Category.create({slug: 'luke', parentCategory: parentCategory}),
"darth/luke",
"it uses the parent slug before the child");
slugFor(Discourse.Category.create({id: 555, parentCategory: parentCategory}),
"darth/555-category",
"it uses the parent slug before the child and then uses id");
parentCategory.set('slug', null);
slugFor(Discourse.Category.create({id: 555, parentCategory: parentCategory}),
"345-category/555-category",
"it uses the parent before the child and uses ids for both");
});
test('findBySlug', function() {
var darth = Discourse.Category.create({id: 1, slug: 'darth'}),
luke = Discourse.Category.create({id: 2, slug: 'luke', parentCategory: darth}),
categoryList = [darth, luke];
this.stub(Discourse.Category, 'list').returns(categoryList);
equal(Discourse.Category.findBySlug('darth'), darth, 'we can find a parent category');
equal(Discourse.Category.findBySlug('luke', 'darth'), luke, 'we can find a child with parent');
blank(Discourse.Category.findBySlug('luke'), 'luke is blank without the parent');
blank(Discourse.Category.findBySlug('luke', 'leia'), 'luke is blank with an incorrect parent');
});