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) { categoryLink: function(category) {
if (!category) return ""; if (!category) return "";
var color = Em.get(category, 'color'); var color = Em.get(category, 'color'),
var textColor = Em.get(category, 'text_color'); textColor = Em.get(category, 'text_color'),
var name = Em.get(category, 'name'); name = Em.get(category, 'name'),
var description = Em.get(category, 'description'); description = Em.get(category, 'description'),
html = "<a href=\"" + Discourse.getURL("/category/") + Discourse.Category.slugFor(category) + "\" class=\"badge-category\" ";
// Build the HTML link
var result = "<a href=\"" + Discourse.getURL("/category/") + Discourse.Category.slugFor(category) + "\" class=\"badge-category\" ";
// Add description if we have it // 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) { avatarUrl: function(template, size) {

View File

@ -132,18 +132,56 @@ Discourse.Category.reopenClass({
slugFor: function(category) { slugFor: function(category) {
if (!category) return ""; if (!category) return "";
var id = Em.get(category, 'id');
var slug = Em.get(category, 'slug'); var parentCategory = Em.get(category, 'parentCategory'),
if (!slug || slug.trim().length === 0) return "" + id + "-category"; result = "";
return slug;
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() { list: function() {
return Discourse.Site.currentProp('categories'); return Discourse.Site.currentProp('categories');
}, },
findBySlugOrId: function(slugOrId) { findBySlug: function(slug, parentSlug) {
// TODO: all our routing around categories need a rethink
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.ajax("/category/" + slugOrId + "/show.json").then(function (result) {
return Discourse.Category.create(result.category); return Discourse.Category.create(result.category);
}); });

View File

@ -24,18 +24,23 @@ Discourse.CategoryList = Ember.ArrayProxy.extend({
Discourse.CategoryList.reopenClass({ Discourse.CategoryList.reopenClass({
categoriesFrom: function(result) { categoriesFrom: function(result) {
var categories = Discourse.CategoryList.create(); var categories = Discourse.CategoryList.create(),
var users = Discourse.Model.extractByKey(result.featured_users, Discourse.User); 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) { 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]; return users[u];
}); });
} }
if (c.topics) { if (c.topics) {
c.topics = _.map(c.topics,function(t) { c.topics = c.topics.map(function(t) {
return Discourse.Topic.create(t); return Discourse.Topic.create(t);
}); });
} }
@ -58,8 +63,9 @@ Discourse.CategoryList.reopenClass({
}, },
list: function(filter) { list: function(filter) {
var self = this; var self = this,
var finder = null; finder = null;
if (filter === 'categories') { if (filter === 'categories') {
finder = PreloadStore.getAndRemove("categories_list", function() { finder = PreloadStore.getAndRemove("categories_list", function() {
return Discourse.ajax("/categories.json"); return Discourse.ajax("/categories.json");

View File

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

View File

@ -83,9 +83,9 @@ Discourse.TopicList = Discourse.Model.extend({
Discourse.TopicList.reopenClass({ Discourse.TopicList.reopenClass({
loadTopics: function(topic_ids, filter) { 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) { Discourse.ajax({url: url}).then(function (result) {
if (result) { if (result) {
// the new topics loaded from the server // the new topics loaded from the server
@ -107,37 +107,45 @@ Discourse.TopicList.reopenClass({
return defer; 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) { topicsFrom: function(result) {
// Stitch together our side loaded data // Stitch together our side loaded data
var categories, topics, users; var categories = Discourse.Category.list(),
categories = this.extractByKey(result.categories, Discourse.Category); users = this.extractByKey(result.users, Discourse.User),
users = this.extractByKey(result.users, Discourse.User); topics = Em.A();
topics = Em.A();
_.each(result.topic_list.topics,function(ft) { return result.topic_list.topics.map(function (t) {
ft.category = categories[ft.category_id]; t.category = categories.findBy('id', t.category_id);
_.each(ft.posters,function(p) { t.posters.forEach(function(p) {
p.user = users[p.user_id]; 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) { 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(); if (list && (list.get('filter') === filter) && window.location.pathname.indexOf('more') > 0) {
var list = session.get('topicList'); list.set('loaded', true);
if (list) { return Ember.RSVP.resolve(list);
if ((list.get('filter') === filter) && window.location.pathname.indexOf('more') > 0) {
list.set('loaded', true);
return Ember.RSVP.resolve(list);
}
} }
session.setProperties({topicList: null, topicListScrollPos: null});
session.set('topicList', null);
session.set('topicListScrollPos', null);
return Discourse.TopicList.find(filter, menuItem.get('excludeCategory')); 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); Discourse.Route.showModal(router, 'editCategory', category);
router.controllerFor('editCategory').set('selectedTab', 'general'); router.controllerFor('editCategory').set('selectedTab', 'general');
} else { } 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.Site.current().updateCategory(c);
Discourse.Route.showModal(router, 'editCategory', c); Discourse.Route.showModal(router, 'editCategory', c);
router.controllerFor('editCategory').set('selectedTab', 'general'); 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 // the homepage is the first item of the 'top_menu' site setting
var settings = Discourse.SiteSettings; var homepage = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
var homepage = settings.top_menu.split("|")[0].split(",")[0];
this.route(homepage, { path: '/' }); this.route(homepage, { path: '/' });
this.route('categories', { path: '/categories' }); this.route('categories', { path: '/categories' });
this.route('category', { path: '/category/:slug/more' });
this.route('category', { path: '/category/:slug' }); 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 // User routes

View File

@ -9,21 +9,7 @@
Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({ Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
model: function(params) { model: function(params) {
var categories = Discourse.Category.list(); return Discourse.Category.findBySlug(Em.get(params, 'slug'), Em.get(params, 'parentSlug'));
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;
}, },
setupController: function(controller, category) { setupController: function(controller, category) {
@ -35,16 +21,18 @@ Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
} }
} }
var listController = this.controllerFor('list'); var listController = this.controllerFor('list'),
var urlId = Discourse.Category.slugFor(category); urlId = Discourse.Category.slugFor(category),
listController.set('filterMode', "category/" + urlId); self = this;
var router = this; listController.set('filterMode', "category/" + urlId);
listController.load("category/" + urlId).then(function(topicList) { listController.load("category/" + urlId).then(function(topicList) {
listController.set('canCreateTopic', topicList.get('can_create_topic')); listController.setProperties({
listController.set('category', category); canCreateTopic: topicList.get('can_create_topic'),
router.controllerFor('listTopics').set('content', topicList); category: category
router.controllerFor('listTopics').set('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 @description = @category.description
end 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) respond(list)
end end
@ -118,7 +123,18 @@ class ListController < ApplicationController
def set_category def set_category
slug = params.fetch(: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 end
def request_is_for_uncategorized? def request_is_for_uncategorized?

View File

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

View File

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

View File

@ -6,9 +6,9 @@ class TopicListItemSerializer < ListableTopicSerializer
:has_best_of, :has_best_of,
:archetype, :archetype,
:rank_details, :rank_details,
:last_poster_username :last_poster_username,
:category_id
has_one :category, serializer: BasicCategorySerializer
has_many :posters, serializer: TopicPosterSerializer, embed: :objects has_many :posters, serializer: TopicPosterSerializer, embed: :objects
def starred 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.rss' => 'list#category_feed', format: :rss, as: 'category_feed'
get 'category/:category' => 'list#category', as: 'category_list' 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' 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. # 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
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 describe 'feed' do
it 'renders RSS' do it 'renders RSS' do
get :category_feed, category: category.slug, format: :rss get :category_feed, category: category.slug, format: :rss

View File

@ -2,13 +2,39 @@ module("Discourse.Category");
test('slugFor', function(){ test('slugFor', function(){
var slugFor = function(args, val, text) { var slugFor = function(cat, val, text) {
equal(Discourse.Category.slugFor(args), val, text); equal(Discourse.Category.slugFor(cat), val, text);
}; };
slugFor({slug: 'hello'}, "hello", "It calculates the proper slug for hello"); slugFor(Discourse.Category.create({slug: 'hello'}), "hello", "It calculates the proper slug for hello");
slugFor({id: 123, slug: ''}, "123-category", "It returns id-category for empty strings"); slugFor(Discourse.Category.create({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({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');
});