diff --git a/app/controllers/post_localizations_controller.rb b/app/controllers/post_localizations_controller.rb new file mode 100644 index 00000000000..8267824b2c7 --- /dev/null +++ b/app/controllers/post_localizations_controller.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class PostLocalizationsController < ApplicationController + before_action :ensure_logged_in + + def create + guardian.ensure_can_localize_content! + + params.require(%i[post_id locale raw]) + PostLocalizationCreator.create( + post_id: params[:post_id], + locale: params[:locale], + raw: params[:raw], + user: current_user, + ) + render json: success_json, status: :created + end + + def update + guardian.ensure_can_localize_content! + + params.require(%i[post_id locale raw]) + PostLocalizationUpdater.update( + post_id: params[:post_id], + locale: params[:locale], + raw: params[:raw], + user: current_user, + ) + render json: success_json, status: :ok + end + + def destroy + guardian.ensure_can_localize_content! + + params.require(%i[post_id locale]) + PostLocalizationDestroyer.destroy( + post_id: params[:post_id], + locale: params[:locale], + acting_user: current_user, + ) + head :no_content + end +end diff --git a/app/controllers/topic_localizations_controller.rb b/app/controllers/topic_localizations_controller.rb new file mode 100644 index 00000000000..337321dc213 --- /dev/null +++ b/app/controllers/topic_localizations_controller.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class TopicLocalizationsController < ApplicationController + before_action :ensure_logged_in + + def create + guardian.ensure_can_localize_content! + + params.require(%i[topic_id locale title]) + TopicLocalizationCreator.create( + topic_id: params[:topic_id], + locale: params[:locale], + title: params[:title], + user: current_user, + ) + render json: success_json, status: :created + end + + def update + guardian.ensure_can_localize_content! + + params.require(%i[topic_id locale title]) + TopicLocalizationUpdater.update( + topic_id: params[:topic_id], + locale: params[:locale], + title: params[:title], + user: current_user, + ) + render json: success_json, status: :ok + end + + def destroy + guardian.ensure_can_localize_content! + + params.require(%i[topic_id locale]) + TopicLocalizationDestroyer.destroy( + topic_id: params[:topic_id], + locale: params[:locale], + acting_user: current_user, + ) + head :no_content + end +end diff --git a/app/models/post.rb b/app/models/post.rb index dbb91538fa0..24f585d2eee 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -67,6 +67,8 @@ class Post < ActiveRecord::Base has_many :user_actions, foreign_key: :target_post_id + has_many :post_localizations, dependent: :destroy + belongs_to :image_upload, class_name: "Upload" has_many :post_hotlinked_media, dependent: :destroy, class_name: "PostHotlinkedMedia" diff --git a/app/models/post_localization.rb b/app/models/post_localization.rb new file mode 100644 index 00000000000..150fff545f5 --- /dev/null +++ b/app/models/post_localization.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class PostLocalization < ActiveRecord::Base + belongs_to :post + + validates :post_version, presence: true + validates :locale, presence: true, length: { maximum: 20 } + validates :raw, presence: true + validates :cooked, presence: true + validates :localizer_user_id, presence: true + validates :locale, uniqueness: { scope: :post_id } +end + +# == Schema Information +# +# Table name: post_localizations +# +# id :bigint not null, primary key +# post_id :integer not null +# post_version :integer not null +# locale :string(20) not null +# raw :text not null +# cooked :text not null +# localizer_user_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_post_localizations_on_post_id (post_id) +# index_post_localizations_on_post_id_and_locale (post_id,locale) UNIQUE +# diff --git a/app/models/topic.rb b/app/models/topic.rb index d06f3f96910..0b8d0f80eab 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -31,6 +31,8 @@ class Topic < ActiveRecord::Base attr_accessor :allowed_user_ids, :allowed_group_ids, :tags_changed, :includes_destination_category + has_many :topic_localizations, dependent: :destroy + def self.max_fancy_title_length 400 end diff --git a/app/models/topic_localization.rb b/app/models/topic_localization.rb new file mode 100644 index 00000000000..5377fc6a2cf --- /dev/null +++ b/app/models/topic_localization.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class TopicLocalization < ActiveRecord::Base + belongs_to :topic + + validates :locale, presence: true, length: { maximum: 20 } + validates :title, presence: true + validates :fancy_title, presence: true + validates :localizer_user_id, presence: true + validates :locale, uniqueness: { scope: :topic_id } +end + +# == Schema Information +# +# Table name: topic_localizations +# +# id :bigint not null, primary key +# topic_id :integer not null +# locale :string(20) not null +# title :string not null +# fancy_title :string not null +# localizer_user_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_topic_localizations_on_topic_id (topic_id) +# index_topic_localizations_on_topic_id_and_locale (topic_id,locale) UNIQUE +# diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 1009393f1da..46cefad0020 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2770,6 +2770,7 @@ en: view_raw_email_allowed_groups: "Groups which can view the raw email content of a post if it was created by an incoming email. This includes email headers and other technical information." experimental_content_localization: "Displays localized content for users based on their language preferences. Such content may include categories, tags, posts, and topics. This feature is under heavy development." rich_editor: "Enable the rich editor so all users can switch between the current Markdown mode and the new rich text editor for more intuitive and user-friendly composition. The rich text editor is under active development, so not all features are supported yet — see Meta for more details." + experimental_content_localization_allowed_groups: 'Groups allowed to update localized content. Requires "experimental content localization" to be enabled.' errors: invalid_css_color: "Invalid color. Enter a color name or hex value." invalid_email: "Invalid email address." @@ -5799,3 +5800,6 @@ en: duplicate_ids: "has duplicate ids" reserved_id: "has a reserved keyword as id: %{id}" unsafe_description: "has an unsafe HTML description" + + content_localization: + not_allowed: "You are not allowed to localize this content." diff --git a/config/routes.rb b/config/routes.rb index 1021e74fedf..94d7f6fd115 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1227,6 +1227,8 @@ Discourse::Application.routes.draw do put "merge_posts" end end + resources :post_localizations, only: %i[create update destroy] + resources :topic_localizations, only: %i[create update destroy] resources :bookmarks, only: %i[create destroy update] do put "toggle_pin" diff --git a/config/site_settings.yml b/config/site_settings.yml index 75cb9389767..71e1251387d 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -3975,3 +3975,9 @@ experimental: experimental_content_localization: client: true default: false + experimental_content_localization_allowed_groups: + type: group_list + list_type: compact + allow_any: false + client: true + default: "1|2" # admin, moderator diff --git a/db/migrate/20250424054312_create_topic_localizations.rb b/db/migrate/20250424054312_create_topic_localizations.rb new file mode 100644 index 00000000000..6e41ec5a845 --- /dev/null +++ b/db/migrate/20250424054312_create_topic_localizations.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateTopicLocalizations < ActiveRecord::Migration[7.2] + def change + create_table :topic_localizations do |t| + t.integer :topic_id, null: false + t.string :locale, null: false, limit: 20 + t.string :title, null: false + t.string :fancy_title, null: false + t.integer :localizer_user_id, null: false + t.timestamps + end + + add_index :topic_localizations, :topic_id + add_index :topic_localizations, %i[topic_id locale], unique: true + end +end diff --git a/db/migrate/20250424054313_create_post_localizations.rb b/db/migrate/20250424054313_create_post_localizations.rb new file mode 100644 index 00000000000..048cd544025 --- /dev/null +++ b/db/migrate/20250424054313_create_post_localizations.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreatePostLocalizations < ActiveRecord::Migration[7.2] + def change + create_table :post_localizations do |t| + t.integer :post_id, null: false + t.integer :post_version, null: false + t.string :locale, null: false, limit: 20 + t.text :raw, null: false + t.text :cooked, null: false + t.integer :localizer_user_id, null: false + t.timestamps + end + + add_index :post_localizations, :post_id + add_index :post_localizations, %i[post_id locale], unique: true + end +end diff --git a/lib/guardian.rb b/lib/guardian.rb index 2619214102e..8888c52d4ae 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -11,6 +11,7 @@ require "guardian/sidebar_guardian" require "guardian/tag_guardian" require "guardian/topic_guardian" require "guardian/user_guardian" +require "guardian/localization_guardian" # The guardian is responsible for confirming access to various site resources and operations class Guardian @@ -21,6 +22,7 @@ class Guardian include GroupGuardian include PostGuardian include PostRevisionGuardian + include LocalizationGuardian include SidebarGuardian include TagGuardian include TopicGuardian diff --git a/lib/guardian/localization_guardian.rb b/lib/guardian/localization_guardian.rb new file mode 100644 index 00000000000..f7b2cbef0f5 --- /dev/null +++ b/lib/guardian/localization_guardian.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module LocalizationGuardian + def can_localize_content? + return false if !SiteSetting.experimental_content_localization + user.in_any_groups?(SiteSetting.experimental_content_localization_allowed_groups_map) + end +end diff --git a/lib/post_localization_creator.rb b/lib/post_localization_creator.rb new file mode 100644 index 00000000000..7c6a7fbb03d --- /dev/null +++ b/lib/post_localization_creator.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class PostLocalizationCreator + def self.create(post_id:, locale:, raw:, user:) + Guardian.new(user).ensure_can_localize_content! + + post = Post.find_by(id: post_id) + raise Discourse::NotFound unless post + + PostLocalization.create!( + post_id: post.id, + post_version: post.version, + locale: locale, + raw: raw, + cooked: PrettyText.cook(raw), + localizer_user_id: user.id, + ) + end +end diff --git a/lib/post_localization_destroyer.rb b/lib/post_localization_destroyer.rb new file mode 100644 index 00000000000..c1352f7378e --- /dev/null +++ b/lib/post_localization_destroyer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class PostLocalizationDestroyer + def self.destroy(post_id:, locale:, acting_user:) + Guardian.new(acting_user).ensure_can_localize_content! + + localization = PostLocalization.find_by(post_id: post_id, locale: locale) + raise Discourse::NotFound unless localization + + localization.destroy + end +end diff --git a/lib/post_localization_updater.rb b/lib/post_localization_updater.rb new file mode 100644 index 00000000000..f4d7cf550b4 --- /dev/null +++ b/lib/post_localization_updater.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class PostLocalizationUpdater + def self.update(post_id:, locale:, raw:, user:) + Guardian.new(user).ensure_can_localize_content! + + localization = PostLocalization.find_by(post_id: post_id, locale: locale) + raise Discourse::NotFound unless localization + + post = Post.find_by(id: post_id) + raise Discourse::NotFound unless post + + localization.raw = raw + localization.cooked = PrettyText.cook(raw) + localization.localizer_user_id = user.id + localization.post_version = post.version + localization.save! + localization + end +end diff --git a/lib/topic_localization_creator.rb b/lib/topic_localization_creator.rb new file mode 100644 index 00000000000..e9535803d58 --- /dev/null +++ b/lib/topic_localization_creator.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class TopicLocalizationCreator + def self.create(topic_id:, locale:, title:, user:) + Guardian.new(user).ensure_can_localize_content! + + topic = Topic.find_by(id: topic_id) + raise Discourse::NotFound unless topic + + TopicLocalization.create!( + topic_id: topic.id, + locale: locale, + title: title, + fancy_title: Topic.fancy_title(title), + localizer_user_id: user.id, + ) + end +end diff --git a/lib/topic_localization_destroyer.rb b/lib/topic_localization_destroyer.rb new file mode 100644 index 00000000000..42f66c9a555 --- /dev/null +++ b/lib/topic_localization_destroyer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class TopicLocalizationDestroyer + def self.destroy(topic_id:, locale:, acting_user:) + Guardian.new(acting_user).ensure_can_localize_content! + + localization = TopicLocalization.find_by(topic_id: topic_id, locale: locale) + raise Discourse::NotFound unless localization + + localization.destroy + end +end diff --git a/lib/topic_localization_updater.rb b/lib/topic_localization_updater.rb new file mode 100644 index 00000000000..29613a2ceef --- /dev/null +++ b/lib/topic_localization_updater.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class TopicLocalizationUpdater + def self.update(topic_id:, locale:, title:, user:) + Guardian.new(user).ensure_can_localize_content! + + localization = TopicLocalization.find_by(topic_id: topic_id, locale: locale) + raise Discourse::NotFound unless localization + + localization.title = title + localization.fancy_title = Topic.fancy_title(title) + localization.localizer_user_id = user.id + localization.save! + localization + end +end diff --git a/spec/fabricators/post_localization_fabricator.rb b/spec/fabricators/post_localization_fabricator.rb new file mode 100644 index 00000000000..8963c013128 --- /dev/null +++ b/spec/fabricators/post_localization_fabricator.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Fabricator(:post_localization) do + post + locale { "ja" } + raw { sequence(:localization_raw) { |n| "これはローカライズされた投稿です。#{n}" } } + cooked { |attrs| "

#{attrs[:raw]}

" } + post_version { |attrs| attrs[:post].version } + localizer_user_id { Discourse.system_user.id } +end diff --git a/spec/fabricators/topic_localization_fabricator.rb b/spec/fabricators/topic_localization_fabricator.rb new file mode 100644 index 00000000000..3e8bb97d62e --- /dev/null +++ b/spec/fabricators/topic_localization_fabricator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +Fabricator(:topic_localization) do + topic + locale { "ja" } + title { sequence(:topic_localization_title) { |n| "これはローカライズされたトピックです。#{n}" } } + fancy_title { |attrs| "a fancy #{attrs[:title]}" } + localizer_user_id { Discourse.system_user.id } +end diff --git a/spec/lib/post_localization_creator_spec.rb b/spec/lib/post_localization_creator_spec.rb new file mode 100644 index 00000000000..56064fc6897 --- /dev/null +++ b/spec/lib/post_localization_creator_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +describe PostLocalizationCreator do + fab!(:user) + fab!(:post) + fab!(:group) + + let(:locale) { "ja" } + let(:raw) { "これは翻訳です。" } + + before do + SiteSetting.experimental_content_localization = true + SiteSetting.experimental_content_localization_allowed_groups = group.id.to_s + group.add(user) + end + + it "creates a post localization record" do + localization = described_class.create(post_id: post.id, locale:, raw:, user:) + + expect(PostLocalization.find(localization.id)).to have_attributes( + post_id: post.id, + locale:, + raw:, + localizer_user_id: user.id, + cooked: PrettyText.cook(raw), + ) + end + + it "raises not found if the post is missing" do + expect { described_class.create(post_id: -1, locale:, raw:, user:) }.to raise_error( + Discourse::NotFound, + ) + end +end diff --git a/spec/lib/post_localization_destroyer_spec.rb b/spec/lib/post_localization_destroyer_spec.rb new file mode 100644 index 00000000000..f6e7e2be00d --- /dev/null +++ b/spec/lib/post_localization_destroyer_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +describe PostLocalizationDestroyer do + fab!(:user) + fab!(:post) + fab!(:group) + fab!(:localization) { Fabricate(:post_localization, post:, locale: "ja") } + + let(:locale) { "ja" } + + before do + SiteSetting.experimental_content_localization = true + SiteSetting.experimental_content_localization_allowed_groups = group.id.to_s + group.add(user) + end + + it "deletes the localization" do + expect { + described_class.destroy(post_id: post.id, locale: locale, acting_user: user) + }.to change { PostLocalization.count }.by(-1) + expect { PostLocalization.find(localization.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "raises not found if the localization is missing" do + expect { + described_class.destroy(post_id: post.id, locale: "nope", acting_user: user) + }.to raise_error(Discourse::NotFound) + end +end diff --git a/spec/lib/post_localization_updater_spec.rb b/spec/lib/post_localization_updater_spec.rb new file mode 100644 index 00000000000..1c8454af1fe --- /dev/null +++ b/spec/lib/post_localization_updater_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +describe PostLocalizationUpdater do + fab!(:user) + fab!(:post) { Fabricate(:post, version: 99) } + fab!(:group) + fab!(:post_localization) do + Fabricate(:post_localization, post: post, locale: "ja", raw: "古いバージョン") + end + + let(:locale) { "ja" } + let(:new_raw) { "新しいバージョンです" } + + before do + SiteSetting.experimental_content_localization = true + SiteSetting.experimental_content_localization_allowed_groups = group.id.to_s + group.add(user) + end + + it "updates an existing localization" do + localization = + described_class.update(post_id: post.id, locale: locale, raw: new_raw, user: user) + + expect(localization).to have_attributes( + raw: new_raw, + cooked: PrettyText.cook(new_raw), + localizer_user_id: user.id, + post_version: post.version, + ) + end + + it "raises not found if the localization is missing" do + expect { + described_class.update(post_id: post.id, locale: "nope", raw: new_raw, user: user) + }.to raise_error(Discourse::NotFound) + end + + it "raises not found if the post is missing" do + expect { + described_class.update(post_id: -1, locale: locale, raw: new_raw, user: user) + }.to raise_error(Discourse::NotFound) + end +end diff --git a/spec/lib/topic_localization_creator_spec.rb b/spec/lib/topic_localization_creator_spec.rb new file mode 100644 index 00000000000..943fa3c100e --- /dev/null +++ b/spec/lib/topic_localization_creator_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +describe TopicLocalizationCreator do + fab!(:user) + fab!(:topic) + fab!(:group) + + let(:locale) { "ja" } + let(:title) { "これは翻訳です" } + + before do + SiteSetting.experimental_content_localization = true + SiteSetting.experimental_content_localization_allowed_groups = group.id.to_s + group.add(user) + end + + it "creates a topic localization record" do + localization = described_class.create(topic_id: topic.id, locale:, title:, user:) + + expect(TopicLocalization.find(localization.id)).to have_attributes( + topic_id: topic.id, + locale:, + title:, + localizer_user_id: user.id, + fancy_title: Topic.fancy_title(title), + ) + end + + it "raises not found if the topic is missing" do + expect { described_class.create(topic_id: -1, locale:, title:, user:) }.to raise_error( + Discourse::NotFound, + ) + end +end diff --git a/spec/lib/topic_localization_destroyer_spec.rb b/spec/lib/topic_localization_destroyer_spec.rb new file mode 100644 index 00000000000..c03db7fe4a2 --- /dev/null +++ b/spec/lib/topic_localization_destroyer_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +describe TopicLocalizationDestroyer do + fab!(:user) + fab!(:group) + fab!(:topic) + fab!(:localization) { Fabricate(:topic_localization, topic:, locale: "ja") } + + let(:locale) { "ja" } + + before do + SiteSetting.experimental_content_localization = true + SiteSetting.experimental_content_localization_allowed_groups = group.id.to_s + group.add(user) + end + + it "deletes the localization" do + expect { + described_class.destroy(topic_id: topic.id, locale: locale, acting_user: user) + }.to change { TopicLocalization.count }.by(-1) + expect { TopicLocalization.find(localization.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "raises not found if the localization is missing" do + expect { + described_class.destroy(topic_id: topic.id, locale: "nope", acting_user: user) + }.to raise_error(Discourse::NotFound) + end +end diff --git a/spec/lib/topic_localization_updater_spec.rb b/spec/lib/topic_localization_updater_spec.rb new file mode 100644 index 00000000000..dac9d82a34a --- /dev/null +++ b/spec/lib/topic_localization_updater_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +describe TopicLocalizationUpdater do + fab!(:user) + fab!(:topic) + fab!(:group) + fab!(:topic_localization) do + Fabricate(:topic_localization, topic:, locale: "ja", title: "古いバージョン") + end + + let(:locale) { "ja" } + let(:new_title) { "新しいバージョンです" } + + before do + SiteSetting.experimental_content_localization = true + SiteSetting.experimental_content_localization_allowed_groups = group.id.to_s + group.add(user) + end + + it "updates an existing localization" do + localization = + described_class.update(topic_id: topic.id, locale: locale, title: new_title, user: user) + + expect(localization).to have_attributes( + title: new_title, + fancy_title: Topic.fancy_title(new_title), + localizer_user_id: user.id, + ) + end + + it "raises not found if the localization is missing" do + expect { + described_class.update(topic_id: topic.id, locale: "nope", title: new_title, user: user) + }.to raise_error(Discourse::NotFound) + end + + it "raises not found if the topic is missing" do + expect { + described_class.update(topic_id: -1, locale: locale, title: new_title, user: user) + }.to raise_error(Discourse::NotFound) + end +end diff --git a/spec/requests/post_localizations_controller_spec.rb b/spec/requests/post_localizations_controller_spec.rb new file mode 100644 index 00000000000..604178f9c0d --- /dev/null +++ b/spec/requests/post_localizations_controller_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +describe PostLocalizationsController do + fab!(:user) + fab!(:group) + fab!(:post_record) { Fabricate(:post, version: 100) } + + let(:locale) { "ja" } + let(:raw) { "これは翻訳です。" } + + before do + SiteSetting.experimental_content_localization = true + SiteSetting.experimental_content_localization_allowed_groups = group.id.to_s + group.add(user) + sign_in(user) + end + + describe "#create" do + it "creates a new localization" do + expect { + post "/post_localizations.json", + params: { + post_id: post_record.id, + locale: locale, + raw: raw, + } + }.to change { PostLocalization.count }.by(1) + expect(response.status).to eq(201) + localization = PostLocalization.last + expect(localization.locale).to eq(locale) + expect(localization.raw).to eq(raw) + expect(localization.post_id).to eq(post_record.id) + expect(localization.post_version).to eq(post_record.version) + expect(localization.localizer_user_id).to eq(user.id) + end + + it "returns forbidden if user not in allowed group" do + group.remove(user) + post "/post_localizations.json", params: { post_id: post_record.id, locale: locale, raw: raw } + expect(response.status).to eq(403) + end + + it "returns not found if post does not exist" do + post "/post_localizations.json", params: { post_id: -1, locale: locale, raw: raw } + expect(response.status).to eq(404) + end + end + + describe "#update" do + fab!(:post_localization) { Fabricate(:post_localization, post: post_record, locale: "ja") } + + it "updates an existing localization" do + put "/post_localizations/#{post_localization.id}.json", + params: { + post_id: post_record.id, + locale: locale, + raw: raw, + } + expect(response.status).to eq(200) + expect(PostLocalization.last.raw).to eq(raw) + end + + it "returns 404 if localization is missing" do + put "/post_localizations.json", params: { post_id: post_record.id, locale: "de", raw: "何か" } + expect(response.status).to eq(404) + end + end + + describe "#destroy" do + fab!(:post_localization) { Fabricate(:post_localization, post: post_record, locale: "ja") } + + it "destroys the localization" do + expect { + delete "/post_localizations/#{post_localization.id}.json", + params: { + post_id: post_record.id, + locale: locale, + } + }.to change { PostLocalization.count }.by(-1) + expect(response.status).to eq(204) + end + + it "returns 404 if localization is missing" do + delete "/post_localizations/289127813837.json", + params: { + post_id: post_record.id, + locale: "nope", + } + expect(response.status).to eq(404) + end + end +end diff --git a/spec/requests/topic_localizations_controller_spec.rb b/spec/requests/topic_localizations_controller_spec.rb new file mode 100644 index 00000000000..95db3bc8a9e --- /dev/null +++ b/spec/requests/topic_localizations_controller_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +describe TopicLocalizationsController do + fab!(:user) + fab!(:group) + fab!(:topic) + + let(:locale) { "ja" } + let(:title) { "これはトピックの翻訳です。" } + + before do + SiteSetting.experimental_content_localization = true + SiteSetting.experimental_content_localization_allowed_groups = group.id.to_s + group.add(user) + sign_in(user) + end + + describe "#create" do + it "creates a new localization" do + expect { + post "/topic_localizations.json", params: { topic_id: topic.id, locale:, title: } + }.to change { TopicLocalization.count }.by(1) + expect(response.status).to eq(201) + expect(TopicLocalization.last).to have_attributes( + locale:, + title:, + topic_id: topic.id, + localizer_user_id: user.id, + ) + end + + it "returns forbidden if user not in allowed group" do + group.remove(user) + expect { + post "/topic_localizations.json", params: { topic_id: topic.id, locale:, title: } + }.not_to change { TopicLocalization.count } + expect(response.status).to eq(403) + end + + it "returns not found if topic does not exist" do + post "/topic_localizations.json", params: { topic_id: -1, locale:, title: } + expect(response.status).to eq(404) + end + end + + describe "#update" do + fab!(:topic_localization) { Fabricate(:topic_localization, topic:, locale: "ja") } + + it "updates an existing localization" do + new_user = Fabricate(:user, groups: [group]) + sign_in(new_user) + + put "/topic_localizations/#{topic_localization.id}.json", + params: { + topic_id: topic.id, + locale:, + title:, + } + expect(response.status).to eq(200) + topic_localization.reload + expect(topic_localization).to have_attributes(locale:, title:, localizer_user_id: new_user.id) + end + + it "returns forbidden if user not in allowed group" do + group.remove(user) + expect { + put "/topic_localizations/#{topic_localization.id}.json", + params: { + topic_id: topic.id, + locale:, + title:, + } + }.not_to change { topic_localization } + expect(response.status).to eq(403) + end + + it "returns not found if localization is missing" do + put "/topic_localizations.json", params: { topic_id: topic.id, locale: "de", title: "何か" } + expect(response.status).to eq(404) + end + end + + describe "#destroy" do + fab!(:topic_localization) { Fabricate(:topic_localization, topic:, locale: "ja") } + + it "destroys the localization" do + expect { + delete "/topic_localizations/#{topic_localization.id}.json", + params: { + topic_id: topic.id, + locale:, + } + }.to change { TopicLocalization.count }.by(-1) + expect(response.status).to eq(204) + end + + it "returns forbidden if user not allowed" do + group.remove(user) + expect { + delete "/topic_localizations/#{topic_localization.id}.json", + params: { + topic_id: topic.id, + locale:, + } + }.not_to change { TopicLocalization.count } + expect(response.status).to eq(403) + end + + it "returns not found if localization is missing" do + expect { + delete "/topic_localizations/219873918.json", params: { topic_id: 219_873_918, locale: } + }.not_to change { TopicLocalization.count } + expect(response.status).to eq(404) + end + end +end