This commit is contained in:
Claire 2025-04-23 08:06:29 +00:00 committed by GitHub
commit b58ce8c360
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 145 additions and 20 deletions

View File

@ -7,7 +7,7 @@ module Admin
def index
authorize :rule, :index?
@rules = Rule.ordered
@rules = Rule.ordered.includes(:translations)
@rule = Rule.new
end
@ -23,7 +23,7 @@ module Admin
if @rule.save
redirect_to admin_rules_path
else
@rules = Rule.ordered
@rules = Rule.ordered.includes(:translations)
render :index
end
end
@ -54,7 +54,7 @@ module Admin
def resource_params
params
.expect(rule: [:text, :hint, :priority])
.expect(rule: [:text, :hint, :priority, translations_attributes: [[:id, :language, :text, :hint, :_destroy]]])
end
end
end

View File

@ -18,6 +18,6 @@ class Api::V1::Instances::RulesController < Api::V1::Instances::BaseController
private
def set_rules
@rules = Rule.ordered
@rules = Rule.ordered.includes(:translations)
end
end

View File

@ -126,7 +126,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def set_rules
@rules = Rule.ordered
@rules = Rule.ordered.includes(:translations)
end
def require_rules_acceptance!

View File

@ -44,6 +44,7 @@ const severityMessages = {
const mapStateToProps = state => ({
server: state.getIn(['server', 'server']),
locale: state.getIn(['meta', 'locale']),
extendedDescription: state.getIn(['server', 'extendedDescription']),
domainBlocks: state.getIn(['server', 'domainBlocks']),
});
@ -91,6 +92,7 @@ class About extends PureComponent {
static propTypes = {
server: ImmutablePropTypes.map,
locale: ImmutablePropTypes.string,
extendedDescription: ImmutablePropTypes.map,
domainBlocks: ImmutablePropTypes.contains({
isLoading: PropTypes.bool,
@ -114,7 +116,7 @@ class About extends PureComponent {
};
render () {
const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;
const { multiColumn, intl, server, extendedDescription, domainBlocks, locale } = this.props;
const isLoading = server.get('isLoading');
return (
@ -168,12 +170,15 @@ class About extends PureComponent {
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
) : (
<ol className='rules-list'>
{server.get('rules').map(rule => (
<li key={rule.get('id')}>
<div className='rules-list__text'>{rule.get('text')}</div>
{rule.get('hint').length > 0 && (<div className='rules-list__hint'>{rule.get('hint')}</div>)}
</li>
))}
{server.get('rules').map(rule => {
const text = rule.getIn(['translations', locale, 'text']) || rule.get('text');
const hint = rule.getIn(['translations', locale, 'hint']) || rule.get('hint');
return (
<li key={rule.get('id')}>
<div className='rules-list__text'>{text}</div>
{hint.length > 0 && (<div className='rules-list__hint'>{hint}</div>)}
</li>
)})}
</ol>
))}
</Section>

View File

@ -12,6 +12,7 @@ import Option from './components/option';
const mapStateToProps = state => ({
rules: state.getIn(['server', 'server', 'rules']),
locale: state.getIn(['meta', 'locale']),
});
class Rules extends PureComponent {
@ -19,6 +20,7 @@ class Rules extends PureComponent {
static propTypes = {
onNextStep: PropTypes.func.isRequired,
rules: ImmutablePropTypes.list,
locale: PropTypes.string,
selectedRuleIds: ImmutablePropTypes.set.isRequired,
onToggle: PropTypes.func.isRequired,
};
@ -34,7 +36,7 @@ class Rules extends PureComponent {
};
render () {
const { rules, selectedRuleIds } = this.props;
const { rules, locale, selectedRuleIds } = this.props;
return (
<>
@ -49,7 +51,7 @@ class Rules extends PureComponent {
value={item.get('id')}
checked={selectedRuleIds.includes(item.get('id'))}
onToggle={this.handleRulesToggle}
label={item.get('text')}
label={item.getIn(['translations', locale, 'text']) || item.get('text')}
multiple
/>
))}

View File

@ -19,6 +19,9 @@ class Rule < ApplicationRecord
self.discard_column = :deleted_at
has_many :translations, inverse_of: :rule, class_name: 'RuleTranslation', dependent: :destroy
accepts_nested_attributes_for :translations, reject_if: :all_blank, allow_destroy: true
validates :text, presence: true, length: { maximum: TEXT_SIZE_LIMIT }
scope :ordered, -> { kept.order(priority: :asc, id: :asc) }

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: rule_translations
#
# id :bigint(8) not null, primary key
# hint :text default(""), not null
# language :string not null
# text :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# rule_id :bigint(8) not null
#
class RuleTranslation < ApplicationRecord
belongs_to :rule
validates :language, presence: true, uniqueness: { scope: :rule_id }
validates :text, presence: true, length: { maximum: Rule::TEXT_SIZE_LIMIT }
end

View File

@ -47,7 +47,7 @@ class InstancePresenter < ActiveModelSerializers::Model
end
def rules
Rule.ordered
Rule.ordered.includes(:translations)
end
def user_count

View File

@ -1,9 +1,15 @@
# frozen_string_literal: true
class REST::RuleSerializer < ActiveModel::Serializer
attributes :id, :text, :hint
attributes :id, :text, :hint, :translations
def id
object.id.to_s
end
def translations
object.translations.to_h do |translation|
[translation.language, { text: translation.text, hint: translation.hint }]
end
end
end

View File

@ -0,0 +1,27 @@
%tr.nested-fields
%td
.fields-row
.fields-row__column.fields-group
= f.input :language,
collection: ui_languages,
include_blank: false,
label_method: ->(locale) { native_locale_name(locale) }
.fields-row__column.fields-group
= f.hidden_field :id if f.object&.persisted? # Required so Rails doesn't put the field outside of the <tr/>
= link_to_remove_association(f, class: 'table-action-link') do
= safe_join([material_symbol('close'), t('filters.index.delete')])
.fields-group
= f.input :text,
label: I18n.t('simple_form.labels.rule.text'),
hint: I18n.t('simple_form.hints.rule.text'),
input_html: { lang: f.object&.language },
wrapper: :with_block_label
.fields-group
= f.input :hint,
label: I18n.t('simple_form.labels.rule.hint'),
hint: I18n.t('simple_form.hints.rule.hint'),
input_html: { lang: f.object&.language },
wrapper: :with_block_label

View File

@ -6,5 +6,26 @@
= render form
%hr.spacer/
%h4= t('admin.rules.translations')
%p.hint= t('admin.rules.translations_explanation')
.table-wrapper
%table.table.keywords-table
%thead
%tr
%th= t('admin.rules.translation')
%th
%tbody
= form.simple_fields_for :translations do |translation|
= render 'translation_fields', f: translation
%tfoot
%tr
%td{ colspan: 3 }
= link_to_add_association form, :translations, class: 'table-action-link', partial: 'translation_fields', 'data-association-insertion-node': '.keywords-table tbody', 'data-association-insertion-method': 'append' do
= safe_join([material_symbol('add'), t('admin.rules.add_translation')])
.actions
= form.button :button, t('generic.save_changes'), type: :submit

View File

@ -18,10 +18,11 @@
%ol.rules-list
- @rules.each do |rule|
- translation = rule.translations.find { |translation| translation.language == I18n.locale.to_s }
%li
%button{ type: 'button', aria: { expanded: 'false' } }
.rules-list__text= rule.text
.rules-list__hint= rule.hint
.rules-list__text= translation&.text || rule.text
.rules-list__hint= translation&.hint || rule.hint
.stacked-actions
- accept_path = @invite_code.present? ? public_invite_url(invite_code: @invite_code, accept: @accept_token) : new_user_registration_path(accept: @accept_token)

View File

@ -786,11 +786,15 @@ en:
title: Roles
rules:
add_new: Add rule
add_translation: Add translation
delete: Delete
description_html: While most claim to have read and agree to the terms of service, usually people do not read through until after a problem arises. <strong>Make it easier to see your server's rules at a glance by providing them in a flat bullet point list.</strong> Try to keep individual rules short and simple, but try not to split them up into many separate items either.
edit: Edit rule
empty: No server rules have been defined yet.
title: Server rules
translation: Translation
translations: Translations
translations_explanation: You can optionally add translations for the rules. The default value will be shown if no translated version is available. Please always ensure any provided translation is in sync with the default value.
settings:
about:
manage_rules: Manage server rules

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class CreateRuleTranslations < ActiveRecord::Migration[8.0]
def change
create_table :rule_translations do |t|
t.text :text, null: false, default: ''
t.text :hint, null: false, default: ''
t.string :language, null: false
t.references :rule, null: false, foreign_key: { on_delete: :cascade }, index: false
t.timestamps
end
add_index :rule_translations, [:rule_id, :language], unique: true
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_04_11_095859) do
ActiveRecord::Schema[8.0].define(version: 2025_04_17_154643) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@ -937,6 +937,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_11_095859) do
t.index ["target_account_id"], name: "index_reports_on_target_account_id"
end
create_table "rule_translations", force: :cascade do |t|
t.text "text", default: "", null: false
t.text "hint", default: "", null: false
t.string "language", null: false
t.bigint "rule_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["rule_id", "language"], name: "index_rule_translations_on_rule_id_and_language", unique: true
end
create_table "rules", force: :cascade do |t|
t.integer "priority", default: 0, null: false
t.datetime "deleted_at", precision: nil
@ -1380,6 +1390,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_11_095859) do
add_foreign_key "reports", "accounts", column: "target_account_id", name: "fk_eb37af34f0", on_delete: :cascade
add_foreign_key "reports", "accounts", name: "fk_4b81f7522c", on_delete: :cascade
add_foreign_key "reports", "oauth_applications", column: "application_id", on_delete: :nullify
add_foreign_key "rule_translations", "rules", on_delete: :cascade
add_foreign_key "scheduled_statuses", "accounts", on_delete: :cascade
add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", name: "fk_957e5bda89", on_delete: :cascade
add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
Fabricator(:rule_translation) do
text 'MyText'
hint 'MyText'
language 'en'
rule { Fabricate.build(:rule) }
end

View File

@ -11,7 +11,8 @@ RSpec.describe REST::RuleSerializer do
it 'returns expected values' do
expect(subject)
.to include(
'id' => be_a(String).and(eq('123'))
'id' => be_a(String).and(eq('123')),
'translations' => be_a(Hash)
)
end
end