FEATURE: Locale support for seeded categories and topics (#7110)

This commit is contained in:
Gerhard Schlager 2019-03-18 21:09:13 +01:00 committed by GitHub
parent d91b47064e
commit 3fd04df781
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 985 additions and 353 deletions

View File

@ -0,0 +1,42 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
export default Ember.Controller.extend(ModalFunctionality, {
loading: true,
reseeding: false,
categories: null,
topics: null,
onShow() {
ajax("/admin/customize/reseed")
.then(result => {
this.setProperties({
categories: result.categories,
topics: result.topics
});
})
.finally(() => this.set("loading", false));
},
_extractSelectedIds(items) {
return items.filter(item => item.selected).map(item => item.id);
},
actions: {
reseed() {
this.set("reseeding", true);
ajax("/admin/customize/reseed", {
data: {
category_ids: this._extractSelectedIds(this.categories),
topic_ids: this._extractSelectedIds(this.topics)
},
method: "POST"
})
.then(
() => this.send("closeModal"),
() => bootbox.alert(I18n.t("generic_error"))
)
.finally(() => this.set("reseeding", false));
}
}
});

View File

@ -1,3 +1,5 @@
import showModal from "discourse/lib/show-modal";
export default Ember.Route.extend({
queryParams: {
q: { replace: true },
@ -13,5 +15,11 @@ export default Ember.Route.extend({
setupController(controller, model) {
controller.set("siteTexts", model);
},
actions: {
showReseedModal() {
showModal("admin-reseed", { admin: true });
}
}
});

View File

@ -0,0 +1,36 @@
{{#d-modal-body title="admin.reseed.modal.title" subtitle="admin.reseed.modal.subtitle" class="reseed-modal"}}
{{#conditional-loading-spinner condition=loading}}
{{#if categories}}
<fieldset>
<legend class="options-group-title">{{i18n "admin.reseed.modal.categories"}}</legend>
{{#each categories as |category|}}
<label>
{{input class="option" type="checkbox" checked=category.selected}}
<span>{{category.name}}</span>
</label>
{{/each}}
</fieldset>
{{/if}}
<br>
{{#if topics}}
<fieldset>
<legend class="options-group-title">{{i18n "admin.reseed.modal.topics"}}</legend>
{{#each topics as |topic|}}
<label>
{{input class="option" type="checkbox" checked=topic.selected}}
<span>{{topic.name}}</span>
</label>
{{/each}}
</fieldset>
{{/if}}
{{/conditional-loading-spinner}}
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action=(action "reseed") class="btn-danger" label="go_ahead" disabled=reseeding}}
{{d-modal-cancel close=(route-action "closeModal")}}
</div>

View File

@ -7,12 +7,20 @@
autofocus="true"
key-up=(action "search")}}
<div class='extra-options'>
<div class="reseed">
{{d-button action=(route-action "showReseedModal")
class="btn-default"
label="admin.reseed.action.label"
title="admin.reseed.action.title"
icon="sync"}}
</div>
<p class="filter-options">
<label>
{{input type="checkbox" checked=overridden click=(action "toggleOverridden")}}
{{i18n 'admin.site_text.show_overriden'}}
</label>
</div>
</p>
</div>
{{#conditional-loading-spinner condition=searching}}

View File

@ -204,11 +204,8 @@ $mobile-breakpoint: 700px;
font-size: $font-0;
width: 50%;
}
.extra-options {
.reseed {
float: right;
input[type="checkbox"] {
margin-right: 0.5em;
}
}
}
.text-highlight {

View File

@ -781,3 +781,15 @@
}
}
}
.reseed-modal {
.options-group-title {
font-size: $font-up-2;
font-weight: bold;
margin: 8px 0;
}
.option {
margin-left: 1em;
}
}

View File

@ -78,6 +78,31 @@ class Admin::SiteTextsController < Admin::AdminController
render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true)
end
def get_reseed_options
render_json_dump(
categories: SeedData::Categories.with_default_locale.reseed_options,
topics: SeedData::Topics.with_default_locale.reseed_options
)
end
def reseed
hijack do
if params[:category_ids].present?
SeedData::Categories.with_default_locale.update(
site_setting_names: params[:category_ids]
)
end
if params[:topic_ids].present?
SeedData::Topics.with_default_locale.update(
site_setting_names: params[:topic_ids]
)
end
render json: success_json
end
end
protected
def record_for(k, value = nil)

View File

@ -4227,6 +4227,17 @@ en:
add: "Add"
filter: "Search (URL or External URL)"
reseed:
action:
label: "Reseed…"
title: "Update content created by Discourse with latest translations"
modal:
title: "Reseed"
subtitle: "Update seeded categories and topics with latest translations"
categories: "Categories"
topics: "Topics"
wizard_js:
wizard:
done: "Done"

View File

@ -578,6 +578,8 @@ en:
[trust]: https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/
admin_quick_start_title: "READ ME FIRST: Admin Quick Start Guide"
category:
topic_prefix: "About the %{category} category"
replace_paragraph: "(Replace this first paragraph with a brief description of your new category. This guidance will appear in the category selection area, so try to keep it below 200 characters.)"
@ -1266,7 +1268,7 @@ en:
site_settings:
censored_words: "Words that will be automatically replaced with &#9632;&#9632;&#9632;&#9632;"
delete_old_hidden_posts: "Auto-delete any hidden posts that stay hidden for more than 30 days."
default_locale: "The default language of this Discourse instance"
default_locale: "The default language of this Discourse instance. You can reseed system generated categories and topics at <a href='%{base_path}/admin/customize/site_texts' target='_blank'>Customize / Text Content</a>."
allow_user_locale: "Allow users to choose their own language interface preference"
set_locale_from_accept_language_header: "set interface language for anonymous users from their web browser's language headers. (EXPERIMENTAL, does not work with anonymous cache)"
support_mixed_text_direction: "Support mixed left-to-right and right-to-left text directions."

View File

@ -226,6 +226,9 @@ Discourse::Application.routes.draw do
delete 'site_texts/:id.json' => 'site_texts#revert', constraints: { id: /[\w.\-\+]+/i }
delete 'site_texts/:id' => 'site_texts#revert', constraints: { id: /[\w.\-\+]+/i }
get 'reseed' => 'site_texts#get_reseed_options'
post 'reseed' => 'site_texts#reseed'
get 'email_templates' => 'email_templates#index'
get 'email_templates/(:id)' => 'email_templates#show', constraints: { id: /[0-9a-z_.]+/ }
put 'email_templates/(:id)' => 'email_templates#update', constraints: { id: /[0-9a-z_.]+/ }

View File

@ -1771,6 +1771,15 @@ uncategorized:
privacy_topic_id:
default: -1
hidden: true
welcome_topic_id:
default: -1
hidden: true
lounge_welcome_topic_id:
default: -1
hidden: true
admin_quick_start_topic_id:
default: -1
hidden: true
bootstrap_mode_min_users:
default: 50

View File

@ -1,27 +0,0 @@
require 'migration/column_dropper'
# fix any bust caches post initial migration
ActiveRecord::Base.send(:subclasses).each { |m| m.reset_column_information }
SiteSetting.refresh!
uncat_id = SiteSetting.uncategorized_category_id
uncat_id = -1 unless Numeric === uncat_id
if uncat_id == -1 || !Category.exists?(uncat_id)
puts "Seeding uncategorized category!"
count = DB.exec "SELECT 1 FROM categories WHERE lower(name) = 'uncategorized'"
name = 'Uncategorized'
name << SecureRandom.hex if count > 0
result = DB.query_single "INSERT INTO categories
(name,color,slug,description,text_color, user_id, created_at, updated_at, position, name_lower)
VALUES ('#{name}', '0088CC', 'uncategorized', '', 'FFFFFF', -1, now(), now(), 1, '#{name.downcase}' )
RETURNING id
"
category_id = result.first.to_i
DB.exec "DELETE FROM site_settings where name = 'uncategorized_category_id'"
DB.exec "INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
VALUES ('uncategorized_category_id', 3, #{category_id}, now(), now())"
end

View File

@ -0,0 +1,3 @@
# fix any bust caches post initial migration
ActiveRecord::Base.send(:subclasses).each { |m| m.reset_column_information }
SiteSetting.refresh!

View File

@ -0,0 +1,3 @@
if !Rails.env.test?
SeedData::Categories.with_default_locale.create
end

View File

@ -1,43 +0,0 @@
unless Rails.env.test?
lounge = Category.find_by(id: SiteSetting.lounge_category_id)
if lounge && lounge.created_at == lounge.updated_at &&
!lounge.group_ids.include?(Group[:trust_level_3].id)
# The category for users with trust level 3 has been created.
# Add initial permissions and description. They can be changed later.
Category.transaction do
lounge.group_names = ['trust_level_3']
unless lounge.save
puts lounge.errors.full_messages
raise "Failed to set permissions on trust level 3 lounge category!"
end
if lounge.topic_id.nil?
creator = PostCreator.new(Discourse.system_user,
raw: I18n.t('vip_category_description'),
title: I18n.t('category.topic_prefix', category: lounge.name),
category: lounge.name,
archetype: Archetype.default,
skip_validations: true
)
post = creator.create
unless post && post.id
puts post.errors.full_messages if post
puts creator.errors.inspect
raise "Failed to create description for trust level 3 lounge!"
end
lounge.topic_id = post.topic.id
unless lounge.save
puts lounge.errors.full_messages
puts "Failed to set the lounge description topic!"
end
# Reset topic count because we don't count the description topic
DB.exec "UPDATE categories SET topic_count = 0 WHERE id = #{lounge.id}"
end
end
end
end

View File

@ -1,31 +0,0 @@
unless Rails.env.test?
meta = Category.find_by(id: SiteSetting.meta_category_id)
if meta && !meta.topic_id
Category.transaction do
creator = PostCreator.new(Discourse.system_user,
raw: I18n.t('meta_category_description'),
title: I18n.t('category.topic_prefix', category: meta.name),
category: meta.name,
archetype: Archetype.default
)
post = creator.create
unless post && post.id
puts post.errors.full_messages if post
puts creator.errors.inspect
raise "Failed meta topic"
end
meta.set_permissions(everyone: :full)
meta.topic_id = post.topic.id
unless meta.save
puts meta.errors.full_messages
puts "Failed to set the meta description and permission!"
end
# Reset topic count because we don't count the description topic
DB.exec "UPDATE categories SET topic_count = 0 WHERE id = #{meta.id}"
end
end
end

View File

@ -1,40 +0,0 @@
unless Rails.env.test?
staff = Category.find_by(id: SiteSetting.staff_category_id)
if staff && !staff.group_ids.include?(Group[:staff].id)
# Add permissions and a description to the Staff category.
Category.transaction do
staff.group_names = ['staff']
unless staff.save
puts staff.errors.full_messages
raise "Failed to set permissions on the Staff category!"
end
if staff.topic_id.nil?
creator = PostCreator.new(Discourse.system_user,
raw: I18n.t('staff_category_description'),
title: I18n.t('category.topic_prefix', category: staff.name),
category: staff.name,
archetype: Archetype.default
)
post = creator.create
unless post && post.id
puts post.errors.full_messages if post
puts creator.errors.inspect
raise "Failed to create description for Staff category!"
end
staff.topic_id = post.topic.id
unless staff.save
puts staff.errors.full_messages
puts "Failed to set the Staff category description topic!"
end
# Reset topic count because we don't count the description topic
DB.exec "UPDATE categories SET topic_count = 0 WHERE id = #{staff.id}"
end
end
end
end

View File

@ -2,64 +2,14 @@ User.reset_column_information
Topic.reset_column_information
Post.reset_column_information
staff = Category.find_by(id: SiteSetting.staff_category_id)
seed_welcome_topics = (Topic.where('id NOT IN (SELECT topic_id from categories where topic_id is not null)').count == 0 && !Rails.env.test?)
if !Rails.env.test?
topics_exist = Topic.where(<<~SQL).exists?
id NOT IN (
SELECT topic_id
FROM categories
WHERE topic_id IS NOT NULL
)
SQL
unless Rails.env.test?
def create_static_page_topic(site_setting_key, title_key, body_key, body_override, category, description, params = {})
unless SiteSetting.send(site_setting_key) > 0
creator = PostCreator.new(Discourse.system_user,
title: I18n.t(title_key, default: I18n.t(title_key, locale: :en)),
raw: body_override.present? ? body_override : I18n.t(body_key, params.merge(default: I18n.t(body_key, params.merge(locale: :en)))),
skip_validations: true,
category: category ? category.name : nil)
post = creator.create
raise "Failed to create the #{description} topic! #{creator.errors.full_messages.join('. ')}" if creator.errors.present?
SiteSetting.send("#{site_setting_key}=", post.topic_id)
_reply = PostCreator.create(Discourse.system_user,
raw: I18n.t('static_topic_first_reply', page_name: I18n.t(title_key, default: I18n.t(title_key, locale: :en))),
skip_validations: true,
topic_id: post.topic_id)
end
end
create_static_page_topic('tos_topic_id', 'tos_topic.title', "tos_topic.body", nil, staff, "terms of service",
company_name: SiteSetting.company_name.presence || "company_name",
base_url: Discourse.base_url,
contact_email: SiteSetting.contact_email.presence || "contact_email",
governing_law: SiteSetting.governing_law.presence || "governing_law",
city_for_disputes: SiteSetting.city_for_disputes.presence || "city_for_disputes")
create_static_page_topic('guidelines_topic_id', 'guidelines_topic.title', "guidelines_topic.body", nil, staff, "guidelines", base_path: Discourse.base_path)
create_static_page_topic('privacy_topic_id', 'privacy_topic.title', "privacy_topic.body", nil, staff, "privacy policy")
end
if seed_welcome_topics
puts "Seeding welcome topics"
post = PostCreator.create(Discourse.system_user, raw: I18n.t('discourse_welcome_topic.body', base_path: Discourse.base_path), title: I18n.t('discourse_welcome_topic.title'), skip_validations: true)
post.topic.update_pinned(true, true)
TopicCustomField.create(topic_id: post.topic.id, name: "is_welcome_topic", value: "true")
lounge = Category.find_by(id: SiteSetting.lounge_category_id)
if lounge
post = PostCreator.create(Discourse.system_user, raw: I18n.t('lounge_welcome.body', base_path: Discourse.base_path), title: I18n.t('lounge_welcome.title'), skip_validations: true, category: lounge.name)
post.topic.update_pinned(true)
end
filename = DiscoursePluginRegistry.seed_data["admin_quick_start_filename"]
if filename.nil? || !File.exists?(filename)
filename = Rails.root + 'docs/ADMIN-QUICK-START-GUIDE.md'
end
welcome = File.read(filename)
PostCreator.create(Discourse.system_user,
raw: welcome,
title: DiscoursePluginRegistry.seed_data["admin_quick_start_title"] || "READ ME FIRST: Admin Quick Start Guide",
skip_validations: true,
category: staff ? staff.name : nil)
SeedData::Topics.with_default_locale.create(include_welcome_topics: !topics_exist)
end

View File

@ -1,38 +1,5 @@
class AddLoungeCategory < ActiveRecord::Migration[4.2]
def up
return if Rails.env.test?
I18n.overrides_disabled do
result = DB.exec "SELECT 1 FROM site_settings where name = 'lounge_category_id'"
if result == 0
description = I18n.t('vip_category_description')
default_name = I18n.t('vip_category_name')
name = if DB.exec("SELECT 1 FROM categories where name = '#{default_name}'") == 0
default_name
else
"CHANGE_ME"
end
result = DB.query_single "INSERT INTO categories
(name, color, text_color, created_at, updated_at, user_id, slug, description, read_restricted, position)
VALUES (:name, 'A461EF', '652D90', now(), now(), -1, '', :description, true, 3)
RETURNING id", name: name, description: description
category_id = result.first.to_i
DB.exec "UPDATE categories SET slug = :slug
WHERE id = :category_id",
slug: Slug.for(name, "#{category_id}-category"), category_id: category_id
execute "INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
VALUES ('lounge_category_id', 3, #{category_id.to_i}, now(), now())"
end
end
end
def down
# Don't reverse this change. There is so much logic around deleting a category that it's messy
# to try to do in sql. The up method will just make sure never to create the category twice.
def change
# replaced by fixture
end
end

View File

@ -1,34 +1,5 @@
class AddMetaCategory < ActiveRecord::Migration[4.2]
def up
return if Rails.env.test?
I18n.overrides_disabled do
result = DB.exec "SELECT 1 FROM site_settings where name = 'meta_category_id'"
if result == 0
description = I18n.t('meta_category_description')
name = I18n.t('meta_category_name')
if DB.exec("SELECT 1 FROM categories where name ilike :name", name: name) == 0
result = DB.query_single "INSERT INTO categories
(name, color, text_color, created_at, updated_at, user_id, slug, description, read_restricted, position)
VALUES (:name, '808281', 'FFFFFF', now(), now(), -1, :slug, :description, true, 1)
RETURNING id", name: name, slug: '', description: description
category_id = result.first.to_i
DB.exec "UPDATE categories SET slug=:slug WHERE id=:category_id",
slug: Slug.for(name, "#{category_id}-category"), category_id: category_id
execute "INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
VALUES ('meta_category_id', 3, #{category_id}, now(), now())"
end
end
end
end
def down
# Don't reverse this change. There is so much logic around deleting a category that it's messy
# to try to do in sql. The up method will just make sure never to create the category twice.
def change
# replaced by fixture
end
end

View File

@ -1,33 +1,5 @@
class AddStaffCategory < ActiveRecord::Migration[4.2]
def up
return if Rails.env.test?
I18n.overrides_disabled do
result = DB.exec "SELECT 1 FROM site_settings where name = 'staff_category_id'"
if result == 0
description = I18n.t('staff_category_description')
name = I18n.t('staff_category_name')
if DB.exec("SELECT 1 FROM categories where name ilike :name", name: name) == 0
result = DB.query_single "INSERT INTO categories
(name, color, text_color, created_at, updated_at, user_id, slug, description, read_restricted, position)
VALUES (:name, 'E45735', 'FFFFFF', now(), now(), -1, '', :description, true, 2)
RETURNING id", name: name, description: description
category_id = result.first.to_i
DB.exec "UPDATE categories SET slug=:slug WHERE id=:category_id",
slug: Slug.for(name, "#{category_id}-category"), category_id: category_id
DB.exec "INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
VALUES ('staff_category_id', 3, #{category_id.to_i}, now(), now())"
end
end
end
end
def down
# Do nothing
def change
# replaced by fixture
end
end

View File

@ -0,0 +1,67 @@
class AddMissingTopicIdSiteSettings < ActiveRecord::Migration[5.2]
def up
# Welcome Topic
execute <<~SQL
INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
SELECT 'welcome_topic_id', 3, topic_id, created_at, updated_at
FROM topic_custom_fields
WHERE name = 'is_welcome_topic' AND value = 'true' AND NOT EXISTS(
SELECT 1
FROM site_settings
WHERE name = 'welcome_topic_id'
)
LIMIT 1
SQL
execute <<~SQL
DELETE FROM topic_custom_fields
WHERE name = 'is_welcome_topic' AND value = 'true'
SQL
# Lounge Welcome Topic
execute <<~SQL
INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
SELECT 'lounge_welcome_topic_id', 3, id, created_at, updated_at
FROM topics
WHERE title = 'Welcome to the Lounge' AND NOT EXISTS(
SELECT 1
FROM site_settings
WHERE name = 'lounge_welcome_topic_id'
) AND category_id = (
SELECT value::INT
FROM site_settings
WHERE name = 'lounge_category_id'
)
ORDER BY created_at
LIMIT 1
SQL
# Admin Quick Start Guide
execute <<~SQL
INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
SELECT 'admin_quick_start_topic_id', 3, id, created_at, updated_at
FROM topics
WHERE title IN ('READ ME FIRST: Admin Quick Start Guide', 'READ ME FIRST: Getting Started') AND NOT EXISTS(
SELECT 1
FROM site_settings
WHERE name = 'admin_quick_start_topic_id'
)
ORDER BY created_at
LIMIT 1
SQL
end
def down
execute <<~SQL
INSERT INTO topic_custom_fields(topic_id, name, value, created_at, updated_at)
SELECT value::INTEGER, 'is_welcome_topic', 'true', created_at, updated_at
FROM site_settings
WHERE name = 'welcome_topic_id'
SQL
execute <<~SQL
DELETE FROM site_settings
WHERE name IN ('welcome_topic_id', 'lounge_welcome_topic_id', 'admin_quick_start_topic_id')
SQL
end
end

View File

@ -29,11 +29,9 @@ class IntroductionUpdater
end
def find_welcome_post
topic_id = TopicCustomField
.where(name: "is_welcome_topic", value: "true")
.pluck(:topic_id)
topic_id = SiteSetting.welcome_topic_id
if topic_id.blank?
if topic_id <= 0
title = I18n.t("discourse_welcome_topic.title")
topic_id = find_topic_id(title)
end

174
lib/seed_data/categories.rb Normal file
View File

@ -0,0 +1,174 @@
module SeedData
class Categories
def self.with_default_locale
SeedData::Categories.new(SiteSetting.default_locale)
end
def initialize(locale)
@locale = locale
end
def create(site_setting_names: nil)
I18n.with_locale(@locale) do
categories(site_setting_names).each { |params| create_category(params) }
end
end
def update(site_setting_names: nil, skip_changed: false)
I18n.with_locale(@locale) do
categories(site_setting_names).each do |params|
params.slice!(:site_setting_name, :name, :description)
params[:skip_changed] = skip_changed
update_category(params)
end
end
end
def reseed_options
I18n.with_locale(@locale) do
categories.map do |params|
category = find_category(params[:site_setting_name])
next unless category
{
id: params[:site_setting_name],
name: category.name,
selected: unchanged?(category)
}
end.compact
end
end
private
def categories(site_setting_names = nil)
categories = [
{
site_setting_name: 'uncategorized_category_id',
name: I18n.t('uncategorized_category_name'),
description: nil,
position: 0,
color: '0088CC',
text_color: 'FFFFFF',
permissions: { everyone: :full },
force_permissions: true,
force_existence: true
},
{
site_setting_name: 'meta_category_id',
name: I18n.t('meta_category_name'),
description: I18n.t('meta_category_description'),
position: 1,
color: '808281',
text_color: 'FFFFFF',
permissions: { everyone: :full },
force_permissions: true
},
{
site_setting_name: 'staff_category_id',
name: I18n.t('staff_category_name'),
description: I18n.t('staff_category_description'),
position: 2,
color: 'E45735',
text_color: 'FFFFFF',
permissions: { staff: :full },
force_permissions: true
},
{
site_setting_name: 'lounge_category_id',
name: I18n.t('vip_category_name'),
description: I18n.t('vip_category_description'),
position: 3,
color: 'A461EF',
text_color: '652D90',
permissions: { trust_level_3: :full },
force_permissions: false
}
]
if site_setting_names
categories.select! { |c| site_setting_names.include?(c[:site_setting_name]) }
end
categories
end
def create_category(site_setting_name:, name:, description:, position:, color:, text_color:,
permissions:, force_permissions:, force_existence: false)
category_id = SiteSetting.send(site_setting_name)
if should_create_category?(category_id, force_existence)
category = Category.new(
name: unused_category_name(category_id, name),
description: description,
user_id: Discourse::SYSTEM_USER_ID,
position: position,
color: color,
text_color: text_color
)
category.skip_category_definition = true if description.blank?
category.set_permissions(permissions)
category.save!
SiteSetting.send("#{site_setting_name}=", category.id)
elsif category = Category.find_by(id: category_id)
if description.present? && (category.topic_id.blank? || !Topic.exists?(category.topic_id))
category.description = description
category.create_category_definition
end
if force_permissions
category.set_permissions(permissions)
category.save! if category.changed?
end
end
end
def should_create_category?(category_id, force_existence)
if category_id > 0
force_existence ? !Category.exists?(category_id) : false
else
true
end
end
def unused_category_name(category_id, name)
category_exists = Category.where(
'id <> :id AND LOWER(name) = :name',
id: category_id,
name: name.downcase
).exists?
category_exists ? "#{name}#{SecureRandom.hex}" : name
end
def update_category(site_setting_name:, name:, description:, skip_changed:)
category = find_category(site_setting_name)
return if !category || (skip_changed && !unchanged?(category))
name = unused_category_name(category.id, name)
category.name = name
category.slug = Slug.for(name, '')
category.save!
if description.present? && description_post = category&.topic&.first_post
changes = { title: I18n.t("category.topic_prefix", category: name), raw: description }
description_post.revise(Discourse.system_user, changes, skip_validations: true)
end
end
def find_category(site_setting_name)
category_id = SiteSetting.send(site_setting_name)
Category.find_by(id: category_id) if category_id > 0
end
def unchanged?(category)
if description_post = category&.topic&.first_post
return description_post.last_editor_id == Discourse::SYSTEM_USER_ID
end
true
end
end
end

197
lib/seed_data/topics.rb Normal file
View File

@ -0,0 +1,197 @@
module SeedData
class Topics
def self.with_default_locale
SeedData::Topics.new(SiteSetting.default_locale)
end
def initialize(locale)
@locale = locale
end
def create(site_setting_names: nil, include_welcome_topics: true)
I18n.with_locale(@locale) do
topics(site_setting_names, include_welcome_topics).each do |params|
create_topic(params)
end
end
end
def update(site_setting_names: nil, skip_changed: false)
I18n.with_locale(@locale) do
topics(site_setting_names).each do |params|
params.except!(:category, :after_create)
params[:skip_changed] = skip_changed
update_topic(params)
end
end
end
def reseed_options
I18n.with_locale(@locale) do
topics.map do |params|
post = find_post(params[:site_setting_name])
next unless post
{
id: params[:site_setting_name],
name: post.topic.title,
selected: unchanged?(post)
}
end.compact
end
end
private
def topics(site_setting_names = nil, include_welcome_topics = true)
staff_category = Category.find_by(id: SiteSetting.staff_category_id)
topics = [
# Terms of Service
{
site_setting_name: 'tos_topic_id',
title: I18n.t('tos_topic.title'),
raw: I18n.t('tos_topic.body',
company_name: setting_value('company_name'),
base_url: Discourse.base_url,
contact_email: setting_value('contact_email'),
governing_law: setting_value('governing_law'),
city_for_disputes: setting_value('city_for_disputes')
),
category: staff_category,
static_first_reply: true
},
# FAQ/Guidelines
{
site_setting_name: 'guidelines_topic_id',
title: I18n.t('guidelines_topic.title'),
raw: I18n.t('guidelines_topic.body', base_path: Discourse.base_path),
category: staff_category,
static_first_reply: true
},
# Privacy Policy
{
site_setting_name: 'privacy_topic_id',
title: I18n.t('privacy_topic.title'),
raw: I18n.t('privacy_topic.body'),
category: staff_category,
static_first_reply: true
}
]
if include_welcome_topics
# Welcome Topic
topics << {
site_setting_name: 'welcome_topic_id',
title: I18n.t('discourse_welcome_topic.title'),
raw: I18n.t('discourse_welcome_topic.body', base_path: Discourse.base_path),
after_create: proc do |post|
post.topic.update_pinned(true, true)
end
}
# Lounge Welcome Topic
if lounge_category = Category.find_by(id: SiteSetting.lounge_category_id)
topics << {
site_setting_name: 'lounge_welcome_topic_id',
title: I18n.t('lounge_welcome.title'),
raw: I18n.t('lounge_welcome.body', base_path: Discourse.base_path),
category: lounge_category,
after_create: proc do |post|
post.topic.update_pinned(true)
end
}
end
# Admin Quick Start Guide
topics << {
site_setting_name: 'admin_quick_start_topic_id',
title: DiscoursePluginRegistry.seed_data['admin_quick_start_title'] || I18n.t('admin_quick_start_title'),
raw: admin_quick_start_raw,
category: staff_category
}
end
if site_setting_names
topics.select! { |t| site_setting_names.include?(t[:site_setting_name]) }
end
topics
end
def create_topic(site_setting_name:, title:, raw:, category: nil, static_first_reply: false, after_create: nil)
topic_id = SiteSetting.send(site_setting_name)
return if topic_id > 0 || Topic.find_by(id: topic_id)
post = PostCreator.create!(
Discourse.system_user,
title: title,
raw: raw,
skip_validations: true,
category: category&.name
)
if static_first_reply
PostCreator.create!(
Discourse.system_user,
raw: first_reply_raw(title),
skip_validations: true,
topic_id: post.topic_id
)
end
after_create&.call(post)
SiteSetting.send("#{site_setting_name}=", post.topic_id)
end
def update_topic(site_setting_name:, title:, raw:, static_first_reply: false, skip_changed:)
post = find_post(site_setting_name)
return if !post
if !skip_changed || unchanged?(post)
changes = { title: title, raw: raw }
post.revise(Discourse.system_user, changes, skip_validations: true)
end
if static_first_reply && (reply = first_reply(post)) && (!skip_changed || unchanged?(reply))
changes = { raw: first_reply_raw(title) }
reply.revise(Discourse.system_user, changes, skip_validations: true)
end
end
def find_post(site_setting_name)
topic_id = SiteSetting.send(site_setting_name)
Post.find_by(topic_id: topic_id, post_number: 1) if topic_id > 0
end
def unchanged?(post)
post.last_editor_id == Discourse::SYSTEM_USER_ID
end
def setting_value(site_setting_key)
SiteSetting.send(site_setting_key).presence || "<ins>#{site_setting_key}</ins>"
end
def first_reply(post)
Post.find_by(topic_id: post.topic_id, post_number: 2, user_id: Discourse::SYSTEM_USER_ID)
end
def first_reply_raw(topic_title)
I18n.t('static_topic_first_reply', page_name: topic_title)
end
def admin_quick_start_raw
quick_start_filename = DiscoursePluginRegistry.seed_data["admin_quick_start_filename"]
if !quick_start_filename || !File.exist?(quick_start_filename)
# TODO Make the quick start guide translatable
quick_start_filename = File.join(Rails.root, 'docs', 'ADMIN-QUICK-START-GUIDE.md')
end
File.read(quick_start_filename)
end
end
end

View File

@ -55,3 +55,16 @@ task "i18n:check", [:locale] => [:environment] do |_, args|
puts ""
exit 1 unless failed_locales.empty?
end
desc "Update seeded topics and categories with latest translations"
task "i18n:reseed", [:locale] => [:environment] do |_, args|
locale = args[:locale]&.to_sym
if locale.blank? || !I18n.locale_available?(locale)
puts "ERROR: Expecting rake i18n:reseed[locale]"
exit 1
end
SeedData::Categories.new(locale).update
SeedData::Topics.new(locale).update
end

View File

@ -64,39 +64,3 @@ task "topics:apply_autoclose" => :environment do
puts "", "Done"
end
def update_static_page_topic(locale, site_setting_key, title_key, body_key, params = {})
topic = Topic.find(SiteSetting.send(site_setting_key))
if (topic && post = topic.first_post)
post.revise(Discourse.system_user,
title: I18n.t(title_key, locale: locale),
raw: I18n.t(body_key, params.merge(locale: locale)))
puts "", "Topic for #{site_setting_key} updated"
else
puts "", "Topic for #{site_setting_key} not found"
end
end
desc "Update static topics (ToS, Privacy, Guidelines) with latest translated content"
task "topics:update_static", [:locale] => [:environment] do |_, args|
locale = args[:locale]&.to_sym
if locale.blank? || !I18n.locale_available?(locale)
puts "ERROR: Expecting rake topics:update_static[locale]"
exit 1
end
update_static_page_topic(locale, "tos_topic_id", "tos_topic.title", "tos_topic.body",
company_name: SiteSetting.company_name.presence || "company_name",
base_url: Discourse.base_url,
contact_email: SiteSetting.contact_email.presence || "contact_email",
governing_law: SiteSetting.governing_law.presence || "governing_law",
city_for_disputes: SiteSetting.city_for_disputes.presence || "city_for_disputes")
update_static_page_topic(locale, "guidelines_topic_id", "guidelines_topic.title", "guidelines_topic.body",
base_path: Discourse.base_path)
update_static_page_topic(locale, "privacy_topic_id", "privacy_topic.title", "privacy_topic.body")
end

View File

@ -1,5 +1,7 @@
require_dependency 'introduction_updater'
require_dependency 'emoji_set_site_setting'
require_dependency 'seed_data/categories'
require_dependency 'seed_data/topics'
class Wizard
class Builder
@ -26,7 +28,15 @@ class Wizard
step.on_update do |updater|
old_locale = SiteSetting.default_locale
updater.apply_setting(:default_locale)
updater.refresh_required = true if old_locale != updater.fields[:default_locale]
if old_locale != updater.fields[:default_locale]
Scheduler::Defer.later "Reseed" do
SeedData::Categories.with_default_locale.update(skip_changed: true)
SeedData::Topics.with_default_locale.update(skip_changed: true)
end
updater.refresh_required = true
end
end
end
@ -95,7 +105,7 @@ class Wizard
step.on_update do |updater|
update_tos do |raw|
replace_company(updater, raw, 'contact_email')
replace_setting_value(updater, raw, 'contact_email')
end
updater.apply_settings(:contact_email, :contact_url)
@ -111,9 +121,9 @@ class Wizard
step.on_update do |updater|
update_tos do |raw|
replace_company(updater, raw, 'company_name')
replace_company(updater, raw, 'governing_law')
replace_company(updater, raw, 'city_for_disputes')
replace_setting_value(updater, raw, 'company_name')
replace_setting_value(updater, raw, 'governing_law')
replace_setting_value(updater, raw, 'city_for_disputes')
end
updater.apply_settings(:company_name, :governing_law, :city_for_disputes)
@ -273,14 +283,14 @@ class Wizard
protected
def replace_company(updater, raw, field_name)
def replace_setting_value(updater, raw, field_name)
old_value = SiteSetting.send(field_name)
old_value = field_name if old_value.blank?
new_value = updater.fields[field_name.to_sym]
new_value = field_name if new_value.blank?
raw.gsub!(old_value, new_value)
raw.gsub!("<ins>#{old_value}</ins>", new_value) || raw.gsub!(old_value, new_value)
end
def reserved_usernames

View File

@ -12,8 +12,8 @@ describe IntroductionUpdater do
topic
end
it "finds the welcome topic by custom field" do
TopicCustomField.create(topic_id: welcome_topic.id, name: "is_welcome_topic", value: "true")
it "finds the welcome topic by site setting" do
SiteSetting.welcome_topic_id = welcome_topic.id
expect(subject.get_summary).to eq(welcome_post_raw)
end

View File

@ -0,0 +1,153 @@
require 'rails_helper'
require 'seed_data/categories'
describe SeedData::Categories do
subject { SeedData::Categories.with_default_locale }
def create_category(name = "staff_category_id")
subject.create(site_setting_names: [name])
end
def description_post(category)
Post.find_by(topic_id: category.topic_id)
end
describe "#create" do
def permissions(group, type)
{
group_id: Group::AUTO_GROUPS[group],
permission_type: CategoryGroup.permission_types[type]
}
end
it "creates a missing category" do
expect { create_category }
.to change { Category.count }.by(1)
.and change { Topic.count }.by(1)
category = Category.last
expect(category.name).to eq(I18n.t("staff_category_name"))
expect(category.topic_id).to be_present
expect(category.user_id).to eq(Discourse::SYSTEM_USER_ID)
expect(category.category_groups.count).to eq(1)
expect(category.category_groups.first).to have_attributes(permissions(:staff, :full))
expect(Topic.exists?(category.topic_id))
expect(description_post(category).raw).to eq(I18n.t("staff_category_description"))
expect(SiteSetting.staff_category_id).to eq(category.id)
end
context "with existing category" do
before { create_category }
it "does not create another category" do
expect { create_category }
.to change { Category.count }.by(0)
.and change { Topic.count }.by(0)
end
it "creates a missing 'About Category' topic" do
category = Category.last
Topic.delete(category.topic_id)
expect { create_category }
.to change { Category.count }.by(0)
.and change { Topic.count }.by(1)
category.reload
expect(description_post(category).raw).to eq(I18n.t("staff_category_description"))
end
it "overwrites permissions when permissions are forced" do
category = Category.last
category.set_permissions(everyone: :full)
category.save!
expect(category.category_groups.count).to eq(0)
expect { create_category }
.to change { CategoryGroup.count }.by(1)
category.reload
expect(category.category_groups.count).to eq(1)
expect(category.category_groups.first).to have_attributes(permissions(:staff, :full))
end
end
it "does not override permissions of existing category when not forced" do
create_category("lounge_category_id")
category = Category.last
category.set_permissions(trust_level_2: :full)
category.save!
expect(category.category_groups.first).to have_attributes(permissions(:trust_level_2, :full))
expect { create_category("lounge_category_id") }
.to change { CategoryGroup.count }.by(0)
category.reload
expect(category.category_groups.first).to have_attributes(permissions(:trust_level_2, :full))
end
end
describe "#update" do
def update_category(name = "staff_category_id", skip_changed: false)
subject.update(site_setting_names: [name], skip_changed: skip_changed)
end
before do
create_category
Category.last.update!(name: "Foo", slug: "foo")
end
it "updates an existing category" do
category = Category.last
description_post(category).revise(Discourse.system_user, raw: "Description for Foo category.")
update_category
category.reload
expect(category.name).to eq(I18n.t("staff_category_name"))
expect(category.slug).to eq(Slug.for(I18n.t("staff_category_name")))
expect(description_post(category).raw).to eq(I18n.t("staff_category_description"))
end
it "skips category when `skip_changed` is true and description was changed" do
category = Category.last
description_post(category).revise(Fabricate(:admin), raw: "Description for Foo category.")
update_category(skip_changed: true)
category.reload
expect(category.name).to eq("Foo")
expect(category.slug).to eq("foo")
expect(description_post(category).raw).to eq("Description for Foo category.")
end
it "works when the category name is already used by another category" do
Fabricate(:category, name: I18n.t("staff_category_name"))
update_category
category = Category.find(SiteSetting.staff_category_id)
expect(category.name).to_not eq(I18n.t("staff_category_name"))
expect(category.name).to start_with(I18n.t("staff_category_name"))
end
end
describe "#reseed_options" do
it "returns only existing categories as options" do
create_category("meta_category_id")
create_category("lounge_category_id")
Post.last.revise(Fabricate(:admin), raw: "Hello world")
expected_options = [
{ id: "uncategorized_category_id", name: I18n.t("uncategorized_category_name"), selected: true },
{ id: "meta_category_id", name: I18n.t("meta_category_name"), selected: true },
{ id: "lounge_category_id", name: I18n.t("vip_category_name"), selected: false }
]
expect(subject.reseed_options).to eq(expected_options)
end
end
end

View File

@ -0,0 +1,122 @@
require 'rails_helper'
require 'seed_data/topics'
describe SeedData::Topics do
subject { SeedData::Topics.with_default_locale }
def create_topic(name = "welcome_topic_id")
subject.create(site_setting_names: [name])
end
describe "#create" do
it "creates a missing topic" do
expect { create_topic }
.to change { Topic.count }.by(1)
.and change { Post.count }.by(1)
topic = Topic.last
expect(topic.title).to eq(I18n.t("discourse_welcome_topic.title"))
expect(topic.first_post.raw).to eq(I18n.t('discourse_welcome_topic.body', base_path: Discourse.base_path).rstrip)
expect(topic.category_id).to eq(SiteSetting.uncategorized_category_id)
expect(topic.user_id).to eq(Discourse::SYSTEM_USER_ID)
expect(topic.pinned_globally).to eq(true)
expect(topic.pinned_at).to be_present
expect(topic.pinned_until).to be_nil
expect(SiteSetting.welcome_topic_id).to eq(topic.id)
end
it "creates a missing topic and a reply when `static_first_reply` is true" do
staff_category = Fabricate(:category, name: "Staff")
SiteSetting.staff_category_id = staff_category.id
expect { create_topic("privacy_topic_id") }
.to change { Topic.count }.by(1)
.and change { Post.count }.by(2)
topic = Topic.last
expect(topic.category_id).to eq(SiteSetting.staff_category_id)
expect(topic.posts_count).to eq(2)
expect(topic.pinned_globally).to eq(false)
expect(topic.pinned_at).to be_nil
expect(topic.pinned_until).to be_nil
post = Post.last
expect(post.topic_id).to eq(topic.id)
expect(post.user_id).to eq(Discourse::SYSTEM_USER_ID)
expect(post.raw).to eq(I18n.t("static_topic_first_reply", page_name: topic.title).rstrip)
end
it "does not create a topic when it already exists" do
topic = Fabricate(:topic)
SiteSetting.welcome_topic_id = topic.id
expect { create_topic }.to_not change { Topic.count }
end
it "does not create a topic when the site setting points to non-existent topic" do
SiteSetting.welcome_topic_id = (Topic.maximum(:id) || 0) + 1
expect { create_topic }.to_not change { Topic.count }
end
end
describe "#update" do
def update_topic(name = "welcome_topic_id", skip_changed: false)
subject.update(site_setting_names: [name], skip_changed: skip_changed)
end
it "updates the changed topic" do
create_topic
topic = Topic.last
topic.update!(title: "New topic title")
topic.first_post.revise(Discourse.system_user, raw: "New text of first post.")
update_topic
topic.reload
expect(topic.title).to eq(I18n.t("discourse_welcome_topic.title"))
expect(topic.first_post.raw).to eq(I18n.t('discourse_welcome_topic.body', base_path: Discourse.base_path).rstrip)
end
it "updates an existing first reply when `static_first_reply` is true" do
create_topic("privacy_topic_id")
post = Post.last
post.revise(Discourse.system_user, raw: "New text of first reply.")
update_topic("privacy_topic_id")
post.reload
expect(post.raw).to eq(I18n.t("static_topic_first_reply", page_name: I18n.t('privacy_topic.title')).rstrip)
end
it "does not update a change topic and `skip_changed` is true" do
create_topic
topic = Topic.last
topic.update!(title: "New topic title")
topic.first_post.revise(Fabricate(:admin), raw: "New text of first post.")
update_topic(skip_changed: true)
expect(topic.title).to eq("New topic title")
expect(topic.first_post.raw).to eq("New text of first post.")
end
end
describe "#reseed_options" do
it "returns only existing topics as options" do
create_topic("guidelines_topic_id")
create_topic("welcome_topic_id")
Post.last.revise(Fabricate(:admin), title: "Changed Topic Title", raw: "Hello world")
expected_options = [
{ id: "guidelines_topic_id", name: I18n.t("guidelines_topic.title"), selected: true },
{ id: "welcome_topic_id", name: "Changed Topic Title", selected: false }
]
expect(subject.reseed_options).to eq(expected_options)
end
end
end

View File

@ -28,7 +28,11 @@ RSpec.describe Admin::SiteTextsController do
put "/admin/customize/site_texts/some_key.json", params: {
site_text: { value: 'foo' }
}
expect(response.status).to eq(404)
put "/admin/customize/reseed.json", params: {
category_ids: [], topic_ids: []
}
expect(response.status).to eq(404)
end
end
@ -243,5 +247,57 @@ RSpec.describe Admin::SiteTextsController do
expect(json['site_text']['value']).to_not eq(ru_mf_text)
end
end
context "reseeding" do
before do
staff_category = Fabricate(
:category,
name: "Staff EN",
user: Discourse.system_user
)
SiteSetting.staff_category_id = staff_category.id
guidelines_topic = Fabricate(
:topic,
title: "The English Guidelines",
category: @staff_category,
user: Discourse.system_user
)
Fabricate(:post, topic: guidelines_topic, user: Discourse.system_user)
SiteSetting.guidelines_topic_id = guidelines_topic.id
end
describe '#get_reseed_options' do
it 'returns correct json' do
get "/admin/customize/reseed.json"
expect(response.status).to eq(200)
expected_reseed_options = {
categories: [
{ id: "uncategorized_category_id", name: I18n.t("uncategorized_category_name"), selected: true },
{ id: "staff_category_id", name: "Staff EN", selected: true }
],
topics: [{ id: "guidelines_topic_id", name: "The English Guidelines", selected: true }]
}
expect(JSON.parse(response.body, symbolize_names: true)).to eq(expected_reseed_options)
end
end
describe '#reseed' do
it 'reseeds categories and topics' do
SiteSetting.default_locale = :de
post "/admin/customize/reseed.json", params: {
category_ids: ["staff_category_id"],
topic_ids: ["guidelines_topic_id"]
}
expect(response.status).to eq(200)
expect(Category.find(SiteSetting.staff_category_id).name).to eq(I18n.t("staff_category_name"))
expect(Topic.find(SiteSetting.guidelines_topic_id).title).to eq(I18n.t("guidelines_topic.title"))
end
end
end
end
end

View File

@ -13,7 +13,7 @@ QUnit.test("search for a key", async assert => {
assert.ok(exists(".site-text.overridden"));
// Only show overridden
await click(".extra-options input");
await click(".search-area .filter-options input");
assert.equal(
currentURL(),
"/admin/customize/site_texts?overridden=true&q=Test"