FEATURE: new 'categories_and_latest' endpoint

This commit is contained in:
Régis Hanol 2016-08-29 22:47:44 +02:00
parent 6c8e6e9d2a
commit e064e6f7a3
10 changed files with 145 additions and 71 deletions

View file

@ -2,27 +2,6 @@ import { ajax } from 'discourse/lib/ajax';
import RestModel from 'discourse/models/rest'; import RestModel from 'discourse/models/rest';
import Model from 'discourse/models/model'; import Model from 'discourse/models/model';
function topicsFrom(result, store) {
if (!result) { return; }
// Stitch together our side loaded data
const categories = Discourse.Category.list(),
users = Model.extractByKey(result.users, Discourse.User);
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];
});
if (t.participants) {
t.participants.forEach(function(p) {
p.user = users[p.user_id];
});
}
return store.createRecord('topic', t);
});
}
const TopicList = RestModel.extend({ const TopicList = RestModel.extend({
canLoadMore: Em.computed.notEmpty("more_topics_url"), canLoadMore: Em.computed.notEmpty("more_topics_url"),
@ -66,8 +45,8 @@ const TopicList = RestModel.extend({
if (result) { if (result) {
// the new topics loaded from the server // the new topics loaded from the server
const newTopics = topicsFrom(result, store), const newTopics = TopicList.topicsFrom(store, result);
topics = self.get("topics"); const topics = self.get("topics");
self.forEachNew(newTopics, function(t) { self.forEachNew(newTopics, function(t) {
t.set('highlight', topicsAdded++ === 0); t.set('highlight', topicsAdded++ === 0);
@ -103,7 +82,7 @@ const TopicList = RestModel.extend({
return ajax({ url }).then(result => { return ajax({ url }).then(result => {
let i = 0; let i = 0;
topicList.forEachNew(topicsFrom(result, store), function(t) { topicList.forEachNew(TopicList.topicsFrom(store, result), function(t) {
// highlight the first of the new topics so we can get a visual feedback // highlight the first of the new topics so we can get a visual feedback
t.set('highlight', true); t.set('highlight', true);
topics.insertAt(i,t); topics.insertAt(i,t);
@ -115,6 +94,26 @@ const TopicList = RestModel.extend({
}); });
TopicList.reopenClass({ TopicList.reopenClass({
topicsFrom(store, result) {
if (!result) { return; }
// Stitch together our side loaded data
const categories = Discourse.Category.list(),
users = Model.extractByKey(result.users, Discourse.User);
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];
});
if (t.participants) {
t.participants.forEach(function(p) {
p.user = users[p.user_id];
});
}
return store.createRecord('topic', t);
});
},
munge(json, store) { munge(json, store) {
json.inserted = json.inserted || []; json.inserted = json.inserted || [];
@ -126,7 +125,7 @@ TopicList.reopenClass({
json.for_period = json.topic_list.for_period; json.for_period = json.topic_list.for_period;
json.loaded = true; json.loaded = true;
json.per_page = json.topic_list.per_page; json.per_page = json.topic_list.per_page;
json.topics = topicsFrom(json, store); json.topics = this.topicsFrom(store, json);
return json; return json;
}, },

View file

@ -3,6 +3,8 @@ 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 TopicList from "discourse/models/topic-list"; import TopicList from "discourse/models/topic-list";
import { ajax } from "discourse/lib/ajax";
import PreloadStore from "preload-store";
const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
renderTemplate() { renderTemplate() {
@ -15,31 +17,66 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
}, },
model() { model() {
return CategoryList.list(this.store, 'categories').then(list => { const style = this.siteSettings.desktop_category_page_style;
const parentCategory = this.get("model.parentCategory");
let promise;
if (parentCategory) {
promise = CategoryList.listForParent(this.store, parentCategory);
} else if (style === "categories_and_latest_topics") {
promise = this._loadCategoriesAndLatestTopics();
} else {
promise = CategoryList.list(this.store);
}
return promise.then(model => {
const tracking = this.topicTrackingState; const tracking = this.topicTrackingState;
if (tracking) { if (tracking) {
tracking.sync(list, "categories"); tracking.sync(model, "categories");
tracking.trackIncoming("categories"); tracking.trackIncoming("categories");
} }
return list; return model;
}); });
}, },
_loadCategoriesAndLatestTopics() {
const categoriesList = PreloadStore.get("categories_list");
const topicListLatest = PreloadStore.get("topic_list_latest");
if (categoriesList && topicListLatest) {
return new Ember.RSVP.Promise(resolve => {
const result = Ember.Object.create({
categories: CategoryList.categoriesFrom(this.store, categoriesList),
topics: TopicList.topicsFrom(this.store, topicListLatest),
can_create_category: categoriesList.can_create_category,
can_create_topic: categoriesList.can_create_topic,
draft_key: categoriesList.draft_key,
draft: categoriesList.draft,
draft_sequence: categoriesList.draft_sequence
});
resolve(result);
});
} else {
return ajax("/categories_and_latest").then(result => {
return Ember.Object.create({
categories: CategoryList.categoriesFrom(this.store, result),
topics: TopicList.topicsFrom(this.store, result),
can_create_category: result.category_list.can_create_category,
can_create_topic: result.category_list.can_create_topic,
draft_key: result.category_list.draft_key,
draft: result.category_list.draft,
draft_sequence: result.category_list.draft_sequence
});
});
}
},
titleToken() { titleToken() {
if (defaultHomepage() === "categories") { return; } if (defaultHomepage() === "categories") { return; }
return I18n.t("filters.categories.title"); return I18n.t("filters.categories.title");
}, },
setupController(controller, model) { setupController(controller, model) {
const style = this.siteSettings.desktop_category_page_style;
if (style === "categories_and_latest_topics" && !this.get("model.parentCategory")) {
model.set("loadingTopics", true);
TopicList.find("latest")
.then(result => model.set("topicList", result))
.finally(() => model.set("loadingTopics", false));
}
controller.set("model", model); controller.set("model", model);
this.controllerFor("navigation/categories").setProperties({ this.controllerFor("navigation/categories").setProperties({
@ -63,12 +100,8 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
// Lesson learned: Don't call `loading` yourself. // Lesson learned: Don't call `loading` yourself.
controller.set("loading", true); controller.set("loading", true);
const parentCategory = this.get("model.parentCategory"); this.model().then(model => {
const promise = parentCategory ? CategoryList.listForParent(this.store, parentCategory) : this.setupController(controller, model);
CategoryList.list(this.store);
promise.then(list => {
this.setupController(controller, list);
controller.send("loadingComplete"); controller.send("loadingComplete");
}); });
}, },

View file

@ -7,25 +7,21 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{#if loadingTopics}} {{#if topics}}
{{loading-spinner}} {{#each topics as |t|}}
{{latest-topic-list-item topic=t}}
{{/each}}
<tr class="more-topics">
<td>
<a href="/latest" class="btn pull-right">{{i18n "more"}}</a>
</td>
</tr>
{{else}} {{else}}
{{#if topics}} <tr class="no-topics">
{{#each topics as |t|}} <td>
{{latest-topic-list-item topic=t}} <h3>{{i18n "topics.none.latest"}}</h3>
{{/each}} </td>
<tr class="more-topics"> </tr>
<td>
<a href="/latest" class="btn pull-right">{{i18n "more"}}</a>
</td>
</tr>
{{else}}
<tr class="no-topics">
<td>
<h3>{{i18n "topics.none.latest"}}</h3>
</td>
</tr>
{{/if}}
{{/if}} {{/if}}
</tbody> </tbody>
</table> </table>

View file

@ -2,6 +2,5 @@
{{component controller.categoryPageStyle {{component controller.categoryPageStyle
categories=model.categories categories=model.categories
latestTopicOnly=controller.latestTopicOnly latestTopicOnly=controller.latestTopicOnly
topics=model.topicList.topics topics=model.topics}}
loadingTopics=model.loadingTopics}}
{{/discovery-categories}} {{/discovery-categories}}

View file

@ -2,10 +2,10 @@ require_dependency 'category_serializer'
class CategoriesController < ApplicationController class CategoriesController < ApplicationController
before_filter :ensure_logged_in, except: [:index, :show, :redirect, :find_by_slug] before_filter :ensure_logged_in, except: [:index, :categories_and_latest, :show, :redirect, :find_by_slug]
before_filter :fetch_category, only: [:show, :update, :destroy] before_filter :fetch_category, only: [:show, :update, :destroy]
before_filter :initialize_staff_action_logger, only: [:create, :update, :destroy] before_filter :initialize_staff_action_logger, only: [:create, :update, :destroy]
skip_before_filter :check_xhr, only: [:index, :redirect] skip_before_filter :check_xhr, only: [:index, :categories_and_latest, :redirect]
def redirect def redirect
redirect_to path("/c/#{params[:path]}") redirect_to path("/c/#{params[:path]}")
@ -16,10 +16,6 @@ class CategoriesController < ApplicationController
@description = SiteSetting.site_description @description = SiteSetting.site_description
include_topics = view_context.mobile_view? ||
params[:include_topics] ||
SiteSetting.desktop_category_page_style == "categories_with_featured_topics".freeze
category_options = { category_options = {
is_homepage: current_homepage == "categories".freeze, is_homepage: current_homepage == "categories".freeze,
parent_category_id: params[:parent_category_id], parent_category_id: params[:parent_category_id],
@ -29,7 +25,7 @@ class CategoriesController < ApplicationController
@category_list = CategoryList.new(guardian, category_options) @category_list = CategoryList.new(guardian, category_options)
@category_list.draft_key = Draft::NEW_TOPIC @category_list.draft_key = Draft::NEW_TOPIC
@category_list.draft_sequence = DraftSequence.current(current_user, 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 @category_list.draft = Draft.get(current_user, Draft::NEW_TOPIC, @category_list.draft_sequence) if current_user
@title = I18n.t('js.filters.categories.title') unless category_options[:is_homepage] @title = I18n.t('js.filters.categories.title') unless category_options[:is_homepage]
@ -50,6 +46,37 @@ class CategoriesController < ApplicationController
end end
end end
def categories_and_latest
discourse_expires_in 1.minute
category_options = {
is_homepage: current_homepage == "categories".freeze,
parent_category_id: params[:parent_category_id],
include_topics: false
}
topic_options = {
per_page: SiteSetting.categories_topics,
no_definitions: true
}
result = CategoryAndTopicLists.new
result.category_list = CategoryList.new(guardian, category_options)
result.topic_list = TopicQuery.new(current_user, topic_options).list_latest
draft_key = Draft::NEW_TOPIC
draft_sequence = DraftSequence.current(current_user, draft_key)
draft = Draft.get(current_user, draft_key, draft_sequence) if current_user
%w{category topic}.each do |type|
result.send(:"#{type}_list").draft = draft
result.send(:"#{type}_list").draft_key = draft_key
result.send(:"#{type}_list").draft_sequence = draft_sequence
end
render_serialized(result, CategoryAndTopicListsSerializer, root: false)
end
def move def move
guardian.ensure_can_create_category! guardian.ensure_can_create_category!
@ -225,4 +252,10 @@ class CategoriesController < ApplicationController
def initialize_staff_action_logger def initialize_staff_action_logger
@staff_action_logger = StaffActionLogger.new(current_user) @staff_action_logger = StaffActionLogger.new(current_user)
end end
def include_topics
view_context.mobile_view? ||
params[:include_topics] ||
SiteSetting.desktop_category_page_style == "categories_with_featured_topics".freeze
end
end end

View file

@ -0,0 +1,5 @@
class CategoryAndTopicLists
include ActiveModel::Serialization
attr_accessor :category_list, :topic_list
end

View file

@ -0,0 +1,4 @@
class CategoryAndTopicListsSerializer < ApplicationSerializer
has_one :category_list, serializer: CategoryListSerializer, embed: :objects
has_one :topic_list, serializer: TopicListSerializer, embed: :objects
end

View file

@ -454,6 +454,8 @@ Discourse::Application.routes.draw do
post "category/:category_id/notifications" => "categories#set_notifications" post "category/:category_id/notifications" => "categories#set_notifications"
put "category/:category_id/slug" => "categories#update_slug" put "category/:category_id/slug" => "categories#update_slug"
get "categories_and_latest" => "categories#categories_and_latest"
get "c/:id/show" => "categories#show" get "c/:id/show" => "categories#show"
get "c/:category_slug/find_by_slug" => "categories#find_by_slug" get "c/:category_slug/find_by_slug" => "categories#find_by_slug"
get "c/:parent_category_slug/:category_slug/find_by_slug" => "categories#find_by_slug" get "c/:parent_category_slug/:category_slug/find_by_slug" => "categories#find_by_slug"

File diff suppressed because one or more lines are too long

View file

@ -133,6 +133,8 @@ export default function() {
return response({ valid: [{ slug: "bug", url: '/c/bugs' }] }); return response({ valid: [{ slug: "bug", url: '/c/bugs' }] });
}); });
this.get("/categories_and_latest", () => response(fixturesByUrl["/categories_and_latest.json"]));
this.put('/categories/:category_id', request => { this.put('/categories/:category_id', request => {
const category = parsePostData(request.requestBody); const category = parsePostData(request.requestBody);