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, NEW_TOPIC_KEY,
} from "discourse/models/composer"; } from "discourse/models/composer";
import RestModel from "discourse/models/rest"; import RestModel from "discourse/models/rest";
import Site from "discourse/models/site";
import UserDraft from "discourse/models/user-draft"; import UserDraft from "discourse/models/user-draft";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
@ -64,6 +65,10 @@ export default class UserDraftsStream extends RestModel {
return; return;
} }
result.categories?.forEach((category) =>
Site.current().updateCategory(category)
);
this.set("hasMore", result.drafts.size >= this.limit); this.set("hasMore", result.drafts.size >= this.limit);
const promises = result.drafts.map((draft) => { 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 { emojiUnescape } from "discourse/lib/text";
import { escapeExpression } from "discourse/lib/utilities"; import { escapeExpression } from "discourse/lib/utilities";
import RestModel from "discourse/models/rest"; import RestModel from "discourse/models/rest";
import Site from "discourse/models/site";
import UserAction from "discourse/models/user-action"; import UserAction from "discourse/models/user-action";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
@ -110,6 +111,11 @@ export default class UserStream extends RestModel {
.then((result) => { .then((result) => {
if (result && result.user_actions) { if (result && result.user_actions) {
const copy = A(); const copy = A();
result.categories?.forEach((category) => {
Site.current().updateCategory(category);
});
result.user_actions.forEach((action) => { result.user_actions.forEach((action) => {
action.title = emojiUnescape(escapeExpression(action.title)); action.title = emojiUnescape(escapeExpression(action.title));
copy.pushObject(UserAction.create(action)); copy.pushObject(UserAction.create(action));

View File

@ -17,7 +17,15 @@ class DraftsController < ApplicationController
limit: fetch_limit_from_params(default: nil, max: INDEX_LIMIT), 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 end
def show def show

View File

@ -28,7 +28,16 @@ class UserActionsController < ApplicationController
} }
stream = UserAction.stream(opts).to_a 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 end
def show def show

View File

@ -211,6 +211,12 @@ class Category < ActiveRecord::Base
) )
SQL 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" delegate :post_template, to: "self.class"
# permission is just used by serialization # permission is just used by serialization

View File

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

View File

@ -1,16 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class AboutSerializer < ApplicationSerializer class AboutSerializer < ApplicationSerializer
class CategoryAboutSerializer < ApplicationSerializer
attributes :id, :name, :color, :slug, :parent_category_id
end
class UserAboutSerializer < BasicUserSerializer class UserAboutSerializer < BasicUserSerializer
attributes :title, :last_seen_at attributes :title, :last_seen_at
end end
class AboutCategoryModsSerializer < ApplicationSerializer class AboutCategoryModsSerializer < ApplicationSerializer
has_one :category, serializer: CategoryAboutSerializer, embed: :objects has_one :category, serializer: CategoryBadgeSerializer, embed: :objects
has_many :moderators, serializer: UserAboutSerializer, embed: :objects has_many :moderators, serializer: UserAboutSerializer, embed: :objects
end 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, :wiki,
:can_edit :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 # Creates a field called field_name_changes with previous and
# current members if a field has changed in this revision # current members if a field has changed in this revision

View File

@ -223,6 +223,19 @@ RSpec.describe Category do
end end
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 describe "security" do
fab!(:category) { Fabricate(:category_with_definition) } fab!(:category) { Fabricate(:category_with_definition) }
fab!(:category_2) { 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.status).to eq(200)
expect(response.parsed_body["drafts"].first["title"]).to eq(nil) expect(response.parsed_body["drafts"].first["title"]).to eq(nil)
end 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 end
describe "#show" do describe "#show" do

View File

@ -28,6 +28,14 @@ RSpec.describe UserActionsController do
expect(actions.first).not_to include "email" expect(actions.first).not_to include "email"
end 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 context "when 'acting_username' is provided" do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }