FIX: Load categories with user activity and drafts (#26553)

When lazy load categories is enabled, categories should be loaded with
user activity items and drafts because the categories may not be
preloaded on the client side.
This commit is contained in:
Bianca Nenciu 2024-04-10 17:35:42 +03:00 committed by GitHub
parent 3733db866c
commit 8ce836c039
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 80 additions and 9 deletions

View File

@ -8,6 +8,7 @@ import {
NEW_TOPIC_KEY,
} from "discourse/models/composer";
import RestModel from "discourse/models/rest";
import Site from "discourse/models/site";
import UserDraft from "discourse/models/user-draft";
import discourseComputed from "discourse-common/utils/decorators";
@ -64,6 +65,10 @@ export default class UserDraftsStream extends RestModel {
return;
}
result.categories?.forEach((category) =>
Site.current().updateCategory(category)
);
this.set("hasMore", result.drafts.size >= this.limit);
const promises = result.drafts.map((draft) => {

View File

@ -5,6 +5,7 @@ import { url } from "discourse/lib/computed";
import { emojiUnescape } from "discourse/lib/text";
import { escapeExpression } from "discourse/lib/utilities";
import RestModel from "discourse/models/rest";
import Site from "discourse/models/site";
import UserAction from "discourse/models/user-action";
import discourseComputed from "discourse-common/utils/decorators";
@ -110,6 +111,11 @@ export default class UserStream extends RestModel {
.then((result) => {
if (result && result.user_actions) {
const copy = A();
result.categories?.forEach((category) => {
Site.current().updateCategory(category);
});
result.user_actions.forEach((action) => {
action.title = emojiUnescape(escapeExpression(action.title));
copy.pushObject(UserAction.create(action));

View File

@ -17,7 +17,15 @@ class DraftsController < ApplicationController
limit: fetch_limit_from_params(default: nil, max: INDEX_LIMIT),
)
render json: { drafts: stream ? serialize_data(stream, DraftSerializer) : [] }
response = { drafts: serialize_data(stream, DraftSerializer) }
if guardian.can_lazy_load_categories?
category_ids = stream.map { |draft| draft.topic&.category_id }.compact.uniq
categories = Category.secured(guardian).with_parents(category_ids)
response[:categories] = serialize_data(categories, CategoryBadgeSerializer)
end
render json: response
end
def show

View File

@ -28,7 +28,16 @@ class UserActionsController < ApplicationController
}
stream = UserAction.stream(opts).to_a
render_serialized(stream, UserActionSerializer, root: "user_actions")
response = { user_actions: serialize_data(stream, UserActionSerializer) }
if guardian.can_lazy_load_categories?
category_ids = stream.map(&:category_id).compact.uniq
categories = Category.secured(guardian).with_parents(category_ids)
response[:categories] = serialize_data(categories, CategoryBadgeSerializer)
end
render json: response
end
def show

View File

@ -211,6 +211,12 @@ class Category < ActiveRecord::Base
)
SQL
scope :with_parents, ->(ids) { where(<<~SQL, ids: ids) }
id IN (:ids)
OR
id IN (SELECT DISTINCT parent_category_id FROM categories WHERE id IN (:ids))
SQL
delegate :post_template, to: "self.class"
# permission is just used by serialization

View File

@ -31,7 +31,7 @@ class PostRevision < ActiveRecord::Base
def categories
return [] if modifications["category_id"].blank?
@categories ||= Category.where(id: modifications["category_id"])
@categories ||= Category.with_parents(modifications["category_id"])
end
def hide!

View File

@ -1,16 +1,12 @@
# frozen_string_literal: true
class AboutSerializer < ApplicationSerializer
class CategoryAboutSerializer < ApplicationSerializer
attributes :id, :name, :color, :slug, :parent_category_id
end
class UserAboutSerializer < BasicUserSerializer
attributes :title, :last_seen_at
end
class AboutCategoryModsSerializer < ApplicationSerializer
has_one :category, serializer: CategoryAboutSerializer, embed: :objects
has_one :category, serializer: CategoryBadgeSerializer, embed: :objects
has_many :moderators, serializer: UserAboutSerializer, embed: :objects
end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class CategoryBadgeSerializer < ApplicationSerializer
attributes :id, :name, :color, :slug, :parent_category_id
end

View File

@ -28,7 +28,7 @@ class PostRevisionSerializer < ApplicationSerializer
:wiki,
:can_edit
has_many :categories, serializer: BasicCategorySerializer, embed: :objects
has_many :categories, serializer: CategoryBadgeSerializer, embed: :objects
# Creates a field called field_name_changes with previous and
# current members if a field has changed in this revision

View File

@ -223,6 +223,19 @@ RSpec.describe Category do
end
end
describe "with_parents" do
fab!(:category)
fab!(:subcategory) { Fabricate(:category, parent_category: category) }
it "returns parent categories and subcategories" do
expect(Category.with_parents([category.id])).to contain_exactly(category)
end
it "returns only categories if top-level categories" do
expect(Category.with_parents([subcategory.id])).to contain_exactly(category, subcategory)
end
end
describe "security" do
fab!(:category) { Fabricate(:category_with_definition) }
fab!(:category_2) { Fabricate(:category_with_definition) }

View File

@ -50,6 +50,21 @@ RSpec.describe DraftsController do
expect(response.status).to eq(200)
expect(response.parsed_body["drafts"].first["title"]).to eq(nil)
end
it "returns categories when lazy load categories is enabled" do
SiteSetting.lazy_load_categories_groups = "#{Group::AUTO_GROUPS[:everyone]}"
category = Fabricate(:category)
topic = Fabricate(:topic, category: category)
Draft.set(topic.user, "topic_#{topic.id}", 0, "{}")
sign_in(topic.user)
get "/drafts.json"
expect(response.status).to eq(200)
draft_keys = response.parsed_body["drafts"].map { |draft| draft["draft_key"] }
expect(draft_keys).to contain_exactly("topic_#{topic.id}")
category_ids = response.parsed_body["categories"].map { |cat| cat["id"] }
expect(category_ids).to contain_exactly(category.id)
end
end
describe "#show" do

View File

@ -28,6 +28,14 @@ RSpec.describe UserActionsController do
expect(actions.first).not_to include "email"
end
it "returns categories when lazy load categories is enabled" do
SiteSetting.lazy_load_categories_groups = "#{Group::AUTO_GROUPS[:everyone]}"
user_actions
expect(response.status).to eq(200)
category_ids = response.parsed_body["categories"].map { |category| category["id"] }
expect(category_ids).to contain_exactly(post.topic.category.id)
end
context "when 'acting_username' is provided" do
let(:user) { Fabricate(:user) }