mirror of
https://github.com/discourse/discourse.git
synced 2026-08-07 13:19:19 +08:00
Related: https://meta.discourse.org/t/multi-language-preferences-for-displaying-localized-content/381116 Allow anon and logged in users to set more than one language that they understand, when content localization is enabled. ### User preferences In user preferences, we now include a Content Languages section when the site has Content Localization enabled. ### Topic menu In topics where there are any languages that's not the user's language, the option will appear to set their preferred content languages. This appears for anon and logged in users, but anon users will be asked to logged in to set the languages. __________ These preferences apply only to topic titles and posts. They do not affect navigational features like categories, tags, or sidebar content. The per-post option to show the original remains independent of the user's preferences. This PR temporarily retains `show_original_content` as a compatibility field computed from `automatically_translate`, keeping cached clients and rolling deployments safe. A follow-up PR will remove the legacy API and frontend compatibility code, then retire the old database column through the staged column-removal process.
592 lines
18 KiB
Ruby
Vendored
592 lines
18 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe CurrentUserSerializer do
|
|
fab!(:user)
|
|
subject(:serializer) { described_class.new(user, scope: guardian, root: false) }
|
|
|
|
let(:guardian) { user.guardian }
|
|
|
|
context "when SSO is not enabled" do
|
|
it "should not include the external_id field" do
|
|
payload = serializer.as_json
|
|
expect(payload).not_to have_key(:external_id)
|
|
end
|
|
end
|
|
|
|
context "when SSO is enabled" do
|
|
let :user do
|
|
user = Fabricate(:user)
|
|
SingleSignOnRecord.create!(user_id: user.id, external_id: "12345", last_payload: "")
|
|
user
|
|
end
|
|
|
|
it "should include the external_id" do
|
|
SiteSetting.discourse_connect_url = "http://example.com/discourse_sso"
|
|
SiteSetting.discourse_connect_secret = "12345678910"
|
|
SiteSetting.enable_discourse_connect = true
|
|
payload = serializer.as_json
|
|
expect(payload[:external_id]).to eq("12345")
|
|
end
|
|
end
|
|
|
|
describe "#top_category_ids" do
|
|
fab!(:category1, :category)
|
|
fab!(:category2, :category)
|
|
fab!(:category3, :category)
|
|
|
|
it "should include empty top_category_ids array" do
|
|
payload = serializer.as_json
|
|
expect(payload[:top_category_ids]).to eq([])
|
|
end
|
|
|
|
it "should include correct id in top_category_ids array" do
|
|
_category = Category.first
|
|
CategoryUser.create!(
|
|
user_id: user.id,
|
|
category_id: category1.id,
|
|
notification_level: CategoryUser.notification_levels[:tracking],
|
|
)
|
|
|
|
CategoryUser.create!(
|
|
user_id: user.id,
|
|
category_id: category2.id,
|
|
notification_level: CategoryUser.notification_levels[:watching],
|
|
)
|
|
|
|
CategoryUser.create!(
|
|
user_id: user.id,
|
|
category_id: category3.id,
|
|
notification_level: CategoryUser.notification_levels[:regular],
|
|
)
|
|
|
|
payload = serializer.as_json
|
|
expect(payload[:top_category_ids]).to eq([category2.id, category1.id])
|
|
end
|
|
end
|
|
|
|
describe "#muted_tag" do
|
|
fab!(:tag)
|
|
|
|
let!(:tag_user) do
|
|
TagUser.create!(
|
|
user_id: user.id,
|
|
notification_level: TagUser.notification_levels[:muted],
|
|
tag_id: tag.id,
|
|
)
|
|
end
|
|
|
|
it "includes muted tags" do
|
|
payload = serializer.as_json
|
|
expect(payload[:muted_tags]).to eq([{ id: tag.id, name: tag.name, slug: tag.slug }])
|
|
end
|
|
end
|
|
|
|
describe "#second_factor_enabled" do
|
|
let(:guardian) { user.guardian }
|
|
let(:json) { serializer.as_json }
|
|
|
|
it "is false by default" do
|
|
expect(json[:second_factor_enabled]).to eq(false)
|
|
end
|
|
|
|
context "when totp enabled" do
|
|
before { User.any_instance.stubs(:totp_enabled?).returns(true) }
|
|
|
|
it "is true" do
|
|
expect(json[:second_factor_enabled]).to eq(true)
|
|
end
|
|
end
|
|
|
|
context "when security_keys enabled" do
|
|
before { User.any_instance.stubs(:security_keys_enabled?).returns(true) }
|
|
|
|
it "is true" do
|
|
expect(json[:second_factor_enabled]).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#groups" do
|
|
it "should only show visible groups" do
|
|
Fabricate(:group, visibility_level: Group.visibility_levels[:public])
|
|
hidden_group = Fabricate(:group, visibility_level: Group.visibility_levels[:owners])
|
|
public_group =
|
|
Fabricate(
|
|
:group,
|
|
visibility_level: Group.visibility_levels[:public],
|
|
name: "UppercaseGroupName",
|
|
)
|
|
hidden_group.add(user)
|
|
public_group.add(user)
|
|
payload = serializer.as_json
|
|
|
|
expect(payload[:groups]).to contain_exactly(
|
|
{ id: public_group.id, name: public_group.name, has_messages: false },
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "#can_ignore_users" do
|
|
let(:guardian) { user.guardian }
|
|
let(:payload) { serializer.as_json }
|
|
|
|
context "when user is a regular one" do
|
|
let(:user) { Fabricate(:user) }
|
|
|
|
it "return false for regular users" do
|
|
expect(payload[:can_ignore_users]).to eq(false)
|
|
end
|
|
end
|
|
|
|
context "when user is a staff member" do
|
|
let(:user) { Fabricate(:moderator) }
|
|
|
|
it "returns true" do
|
|
expect(payload[:can_ignore_users]).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#can_create_admin_invite" do
|
|
let(:payload) { serializer.as_json }
|
|
|
|
it "is true for admins when enable_invite_modal_with_roles is enabled" do
|
|
SiteSetting.enable_invite_modal_with_roles = true
|
|
user.update!(admin: true)
|
|
|
|
expect(payload[:can_create_admin_invite]).to eq(true)
|
|
end
|
|
|
|
it "is absent for admins when enable_invite_modal_with_roles is disabled" do
|
|
SiteSetting.enable_invite_modal_with_roles = false
|
|
user.update!(admin: true)
|
|
|
|
expect(payload).not_to have_key(:can_create_admin_invite)
|
|
end
|
|
|
|
it "is absent for regular users when enable_invite_modal_with_roles is enabled" do
|
|
SiteSetting.enable_invite_modal_with_roles = true
|
|
|
|
expect(payload).not_to have_key(:can_create_admin_invite)
|
|
end
|
|
end
|
|
|
|
describe "#can_review" do
|
|
let(:guardian) { user.guardian }
|
|
let(:payload) { serializer.as_json }
|
|
|
|
context "when user is a regular one" do
|
|
let(:user) { Fabricate(:user) }
|
|
|
|
it "return false for regular users" do
|
|
expect(payload[:can_review]).to eq(false)
|
|
end
|
|
end
|
|
|
|
context "when user is a staff member" do
|
|
let(:user) { Fabricate(:admin) }
|
|
|
|
it "returns true" do
|
|
expect(payload[:can_review]).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#can_set_topic_timer" do
|
|
let(:payload) { serializer.as_json }
|
|
|
|
it "reflects the topic_timers_allowed_groups setting" do
|
|
group = Fabricate(:group)
|
|
group.add(user)
|
|
user.reload
|
|
SiteSetting.topic_timers_allowed_groups = group.id.to_s
|
|
|
|
expect(payload[:can_set_topic_timer]).to eq(true)
|
|
end
|
|
end
|
|
|
|
describe "#pending_posts_count" do
|
|
subject(:pending_posts_count) { serializer.pending_posts_count }
|
|
|
|
let(:user) { Fabricate(:user) }
|
|
|
|
before { user.user_stat.pending_posts_count = 3 }
|
|
|
|
it "serializes 'pending_posts_count'" do
|
|
expect(pending_posts_count).to eq 3
|
|
end
|
|
end
|
|
|
|
describe "#status" do
|
|
fab!(:user_status)
|
|
fab!(:user) { Fabricate(:user, user_status: user_status) }
|
|
let(:serializer) { described_class.new(user, scope: user.guardian, root: false) }
|
|
|
|
it "adds user status when enabled" do
|
|
SiteSetting.enable_user_status = true
|
|
|
|
json = serializer.as_json
|
|
|
|
expect(json[:status]).to_not be_nil do |status|
|
|
expect(status.description).to eq(user_status.description)
|
|
expect(status.emoji).to eq(user_status.emoji)
|
|
end
|
|
end
|
|
|
|
it "doesn't add user status when disabled" do
|
|
SiteSetting.enable_user_status = false
|
|
json = serializer.as_json
|
|
expect(json.keys).not_to include :status
|
|
end
|
|
|
|
it "doesn't add expired user status" do
|
|
SiteSetting.enable_user_status = true
|
|
|
|
user.user_status.ends_at = 1.minute.ago
|
|
serializer = described_class.new(user, scope: user.guardian, root: false)
|
|
json = serializer.as_json
|
|
|
|
expect(json.keys).not_to include :status
|
|
end
|
|
|
|
it "doesn't return status if user doesn't have it set" do
|
|
SiteSetting.enable_user_status = true
|
|
|
|
user.clear_status!
|
|
user.reload
|
|
json = serializer.as_json
|
|
|
|
expect(json.keys).not_to include :status
|
|
end
|
|
end
|
|
|
|
describe "#likes_notifications_disabled" do
|
|
it "is true if the user disables likes notifications" do
|
|
user.user_option.update!(
|
|
like_notification_frequency: UserOption.like_notification_frequency_type[:never],
|
|
)
|
|
expect(serializer.as_json[:user_option][:likes_notifications_disabled]).to eq(true)
|
|
end
|
|
|
|
it "is false if the user doesn't disable likes notifications" do
|
|
user.user_option.update!(
|
|
like_notification_frequency: UserOption.like_notification_frequency_type[:always],
|
|
)
|
|
expect(serializer.as_json[:user_option][:likes_notifications_disabled]).to eq(false)
|
|
user.user_option.update!(
|
|
like_notification_frequency:
|
|
UserOption.like_notification_frequency_type[:first_time_and_daily],
|
|
)
|
|
expect(serializer.as_json[:user_option][:likes_notifications_disabled]).to eq(false)
|
|
user.user_option.update!(
|
|
like_notification_frequency: UserOption.like_notification_frequency_type[:first_time],
|
|
)
|
|
expect(serializer.as_json[:user_option][:likes_notifications_disabled]).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe "content localization preferences" do
|
|
it "computes the legacy JSON preference from automatically_translate" do
|
|
user.user_option.automatically_translate = false
|
|
user.user_option.show_original_content = false
|
|
|
|
expect(serializer.as_json[:user_option]).to include(
|
|
automatically_translate: false,
|
|
show_original_content: true,
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "#associated_account_ids" do
|
|
before do
|
|
UserAssociatedAccount.create(
|
|
user_id: user.id,
|
|
provider_name: "twitter",
|
|
provider_uid: "1",
|
|
info: {
|
|
nickname: "sam",
|
|
},
|
|
)
|
|
end
|
|
|
|
it "should not include associated account ids by default" do
|
|
expect(serializer.as_json[:associated_account_ids]).to be_nil
|
|
end
|
|
|
|
it "should include associated account ids when site setting enabled" do
|
|
SiteSetting.include_associated_account_ids = true
|
|
expect(serializer.as_json[:associated_account_ids]).to eq({ "twitter" => "1" })
|
|
end
|
|
end
|
|
|
|
describe "#new_personal_messages_notifications_count" do
|
|
fab!(:notification) do
|
|
Fabricate(
|
|
:notification,
|
|
user: user,
|
|
read: false,
|
|
notification_type: Notification.types[:private_message],
|
|
)
|
|
end
|
|
|
|
it "is included when sidebar is enabled" do
|
|
SiteSetting.navigation_menu = "sidebar"
|
|
|
|
expect(serializer.as_json[:new_personal_messages_notifications_count]).to eq(1)
|
|
end
|
|
end
|
|
|
|
include_examples "User Sidebar Serializer Attributes", described_class
|
|
|
|
describe "#sidebar_sections" do
|
|
fab!(:group)
|
|
fab!(:sidebar_section) { Fabricate(:sidebar_section, user: user) }
|
|
|
|
it "eager loads sidebar_urls" do
|
|
custom_sidebar_section_link_1 =
|
|
Fabricate(:custom_sidebar_section_link, user: user, sidebar_section: sidebar_section)
|
|
|
|
# warmup
|
|
described_class.new(user, scope: user.guardian, root: false).as_json
|
|
|
|
initial_count =
|
|
track_sql_queries do
|
|
serialized = described_class.new(user, scope: user.guardian, root: false).as_json
|
|
|
|
expect(serialized[:sidebar_sections].count).to eq(2)
|
|
|
|
expect(serialized[:sidebar_sections].last[:links].map { |link| link[:id] }).to eq(
|
|
[custom_sidebar_section_link_1.linkable.id],
|
|
)
|
|
end.count
|
|
|
|
custom_sidebar_section_link_2 =
|
|
Fabricate(:custom_sidebar_section_link, user: user, sidebar_section: sidebar_section)
|
|
|
|
final_count =
|
|
track_sql_queries do
|
|
serialized = described_class.new(user, scope: user.guardian, root: false).as_json
|
|
|
|
expect(serialized[:sidebar_sections].count).to eq(2)
|
|
|
|
expect(serialized[:sidebar_sections].last[:links].map { |link| link[:id] }).to eq(
|
|
[custom_sidebar_section_link_1.linkable.id, custom_sidebar_section_link_2.linkable.id],
|
|
)
|
|
end.count
|
|
|
|
expect(initial_count).to eq(final_count)
|
|
end
|
|
end
|
|
|
|
describe "#can_create_category" do
|
|
let(:payload) { serializer.as_json }
|
|
|
|
context "when user is an admin" do
|
|
let(:user) { Fabricate(:admin) }
|
|
|
|
it "returns true" do
|
|
expect(payload[:can_create_category]).to eq(true)
|
|
end
|
|
end
|
|
|
|
context "when user is a moderator and moderators_manage_categories is enabled" do
|
|
let(:user) { Fabricate(:moderator) }
|
|
|
|
before { SiteSetting.moderators_manage_categories = true }
|
|
|
|
it "returns true" do
|
|
expect(payload[:can_create_category]).to eq(true)
|
|
end
|
|
end
|
|
|
|
context "when user is a moderator and moderators_manage_categories is disabled" do
|
|
let(:user) { Fabricate(:moderator) }
|
|
|
|
before { SiteSetting.moderators_manage_categories = false }
|
|
|
|
it "is not included" do
|
|
expect(payload).not_to have_key(:can_create_category)
|
|
end
|
|
end
|
|
|
|
context "when user is a regular user" do
|
|
it "is not included" do
|
|
expect(payload).not_to have_key(:can_create_category)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#can_see_ip" do
|
|
let(:payload) { serializer.as_json }
|
|
|
|
context "when user is an admin" do
|
|
let(:user) { Fabricate(:admin) }
|
|
|
|
it "includes can_see_ip as true" do
|
|
expect(payload[:can_see_ip]).to eq(true)
|
|
end
|
|
end
|
|
|
|
context "when user is a moderator and moderators_view_ips is enabled" do
|
|
let(:user) { Fabricate(:moderator) }
|
|
|
|
before { SiteSetting.moderators_view_ips = true }
|
|
|
|
it "includes can_see_ip as true" do
|
|
expect(payload[:can_see_ip]).to eq(true)
|
|
end
|
|
end
|
|
|
|
context "when user is a moderator and moderators_view_ips is disabled" do
|
|
let(:user) { Fabricate(:moderator) }
|
|
|
|
before { SiteSetting.moderators_view_ips = false }
|
|
|
|
it "does not include can_see_ip" do
|
|
expect(payload).not_to have_key(:can_see_ip)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#featured_topic" do
|
|
fab!(:featured_topic, :topic)
|
|
|
|
before { user.user_profile.update!(featured_topic_id: featured_topic.id) }
|
|
|
|
it "includes the featured topic" do
|
|
payload = serializer.as_json
|
|
|
|
expect(payload[:featured_topic]).to_not be_nil
|
|
expect(payload[:featured_topic][:id]).to eq(featured_topic.id)
|
|
expect(payload[:featured_topic][:title]).to eq(featured_topic.title)
|
|
expect(payload[:featured_topic].keys).to contain_exactly(
|
|
:id,
|
|
:title,
|
|
:fancy_title,
|
|
:slug,
|
|
:posts_count,
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "#show_site_owner_onboarding" do
|
|
fab!(:admin)
|
|
fab!(:topic)
|
|
fab!(:another_admin, :admin)
|
|
|
|
let(:admin_serializer) { described_class.new(admin, scope: Guardian.new(admin), root: false) }
|
|
|
|
before { SiteSetting.enable_site_owner_onboarding = true }
|
|
|
|
it "is not included for non-admin users" do
|
|
payload = serializer.as_json
|
|
expect(payload).not_to have_key(:show_site_owner_onboarding)
|
|
end
|
|
|
|
it "is not included when setting is disabled" do
|
|
SiteSetting.enable_site_owner_onboarding = false
|
|
payload = admin_serializer.as_json
|
|
expect(payload).not_to have_key(:show_site_owner_onboarding)
|
|
end
|
|
|
|
it "is true for the first admin on a new site" do
|
|
payload = admin_serializer.as_json
|
|
expect(payload[:show_site_owner_onboarding]).to eq(true)
|
|
end
|
|
|
|
it "is not included for a second admin" do
|
|
serializer2 =
|
|
described_class.new(another_admin, scope: Guardian.new(another_admin), root: false)
|
|
payload = serializer2.as_json
|
|
expect(payload).not_to have_key(:show_site_owner_onboarding)
|
|
end
|
|
|
|
it "is not included when the site is older than the max days setting" do
|
|
SiteSetting.site_owner_onboarding_max_days = 5
|
|
admin.created_at = 6.days.ago
|
|
admin.save!
|
|
payload = admin_serializer.as_json
|
|
expect(payload).not_to have_key(:show_site_owner_onboarding)
|
|
end
|
|
end
|
|
|
|
describe "#has_new_upcoming_changes" do
|
|
def serialize_for(user)
|
|
described_class.new(user, scope: user.guardian, root: false).as_json
|
|
end
|
|
|
|
it "is false for an admin created during the new-site window, regardless of `added` events" do
|
|
Discourse.stubs(:site_creation_date).returns(10.minutes.ago)
|
|
admin = Fabricate(:admin)
|
|
UpcomingChangeEvent.create!(
|
|
event_type: :added,
|
|
upcoming_change_name: "some_setting",
|
|
created_at: 5.minutes.ago,
|
|
)
|
|
UpcomingChangeEvent.create!(
|
|
event_type: :added,
|
|
upcoming_change_name: "future_setting",
|
|
created_at: 1.minute.from_now,
|
|
)
|
|
expect(serialize_for(admin)[:has_new_upcoming_changes]).to eq(false)
|
|
end
|
|
|
|
it "ignores `added` events that predate the user when no last_visited is set" do
|
|
Discourse.stubs(:site_creation_date).returns(2.years.ago)
|
|
UpcomingChangeEvent.create!(
|
|
event_type: :added,
|
|
upcoming_change_name: "old_setting",
|
|
created_at: 1.year.ago,
|
|
)
|
|
admin = Fabricate(:admin)
|
|
expect(serialize_for(admin)[:has_new_upcoming_changes]).to eq(false)
|
|
end
|
|
|
|
it "shows `added` events created after the user when no last_visited is set" do
|
|
Discourse.stubs(:site_creation_date).returns(2.years.ago)
|
|
admin = Fabricate(:admin)
|
|
UpcomingChangeEvent.create!(
|
|
event_type: :added,
|
|
upcoming_change_name: "fresh_setting",
|
|
created_at: 1.minute.from_now,
|
|
)
|
|
expect(serialize_for(admin)[:has_new_upcoming_changes]).to eq(true)
|
|
end
|
|
|
|
it "ignores backfilled `added` events" do
|
|
Discourse.stubs(:site_creation_date).returns(2.years.ago)
|
|
admin = Fabricate(:admin)
|
|
UpcomingChangeEvent.create!(
|
|
event_type: :added,
|
|
event_data: {
|
|
backfilled: true,
|
|
},
|
|
upcoming_change_name: "backfilled_setting",
|
|
created_at: 1.minute.from_now,
|
|
)
|
|
|
|
expect(serialize_for(admin)[:has_new_upcoming_changes]).to eq(false)
|
|
end
|
|
|
|
it "honors last_visited_upcoming_changes_at when set" do
|
|
Discourse.stubs(:site_creation_date).returns(2.years.ago)
|
|
admin = Fabricate(:admin)
|
|
admin.custom_fields["last_visited_upcoming_changes_at"] = 1.hour.ago.iso8601
|
|
admin.save_custom_fields
|
|
|
|
UpcomingChangeEvent.create!(
|
|
event_type: :added,
|
|
upcoming_change_name: "before_visit",
|
|
created_at: 2.hours.ago,
|
|
)
|
|
expect(serialize_for(admin)[:has_new_upcoming_changes]).to eq(false)
|
|
|
|
UpcomingChangeEvent.create!(
|
|
event_type: :added,
|
|
upcoming_change_name: "after_visit",
|
|
created_at: 1.minute.ago,
|
|
)
|
|
expect(serialize_for(admin)[:has_new_upcoming_changes]).to eq(true)
|
|
end
|
|
end
|
|
end
|