Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions app/models/site_setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ class SiteSetting < ActiveRecord::Base
elsif self.data_type == SiteSettings::TypeSupervisor.types[:uploaded_image_list]
upload_ids = self.value.split("|").compact.uniq
UploadReference.ensure_exist!(upload_ids: upload_ids, target: self)
elsif self.data_type == SiteSettings::TypeSupervisor.types[:objects]
upload_ids = extract_upload_ids_from_objects_value
UploadReference.ensure_exist!(upload_ids: upload_ids, target: self) if upload_ids.any?
end
end
end
Expand Down Expand Up @@ -379,6 +382,31 @@ def self.clear_cache!(expire_theme_site_setting_cache: false)
@blocked_attachment_filenames_regex = nil
@allowed_unicode_username_regex = nil
end

private

def extract_upload_ids_from_objects_value
return [] if self.value.blank?

type_hash = SiteSetting.type_supervisor.type_hash(self.name.to_sym)
return [] unless type_hash[:schema]&.dig(:properties)

begin
parsed_value = JSON.parse(self.value)
parsed_value = [parsed_value] unless parsed_value.is_a?(Array)
upload_ids = Set.new

parsed_value.each do |obj|
validator = SchemaSettingsObjectValidator.new(schema: type_hash[:schema], object: obj)

upload_ids.merge(validator.property_values_of_type("upload"))
end

upload_ids.to_a
rescue JSON::ParserError
[]
end
end
end

# == Schema Information
Expand Down
31 changes: 29 additions & 2 deletions app/models/theme_setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ class ThemeSetting < ActiveRecord::Base
after_save :clear_settings_cache

after_save do
if self.data_type == ThemeSetting.types[:upload] && saved_change_to_value?
UploadReference.ensure_exist!(upload_ids: [self.value], target: self)
if saved_change_to_value?
if self.data_type == ThemeSetting.types[:upload]
UploadReference.ensure_exist!(upload_ids: [self.value], target: self)
elsif self.data_type == ThemeSetting.types[:objects]
upload_ids = extract_upload_ids_from_objects_value
UploadReference.ensure_exist!(upload_ids: upload_ids, target: self) if upload_ids.any?
end
end

if theme.theme_modifier_set.refresh_theme_setting_modifiers(
Expand Down Expand Up @@ -55,6 +60,28 @@ def self.guess_type(value)

private

def extract_upload_ids_from_objects_value
return [] if self.value.blank?

schema = theme.settings[self.name.to_sym]&.schema
return [] unless schema&.dig(:properties)

begin
parsed_value = JSON.parse(self.value)
parsed_value = [parsed_value] unless parsed_value.is_a?(Array)
upload_ids = Set.new

parsed_value.each do |obj|
validator = SchemaSettingsObjectValidator.new(schema: schema, object: obj)
upload_ids.merge(validator.property_values_of_type("upload"))
end

upload_ids.to_a
rescue JSON::ParserError
[]
end
end

def json_value_size
if json_value.to_json.size > MAXIMUM_JSON_VALUE_SIZE_BYTES
errors.add(
Expand Down
37 changes: 37 additions & 0 deletions app/models/theme_site_setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@
class ThemeSiteSetting < ActiveRecord::Base
belongs_to :theme

has_many :upload_references, as: :target, dependent: :destroy

after_save do
if saved_change_to_value?
if self.data_type == SiteSettings::TypeSupervisor.types[:upload]
UploadReference.ensure_exist!(upload_ids: [self.value], target: self)
elsif self.data_type == SiteSettings::TypeSupervisor.types[:objects]
upload_ids = extract_upload_ids_from_objects_value
UploadReference.ensure_exist!(upload_ids: upload_ids, target: self)
end
end
end

# Gets a list of themes that have theme site setting records
# and the associated values for those settings, where the
# value is different from the default site setting value.
Expand Down Expand Up @@ -108,6 +121,30 @@ def self.generate_defaults_map
def setting_rb_value
SiteSetting.type_supervisor.to_rb_value(self.name, self.value, self.data_type)
end

private

def extract_upload_ids_from_objects_value
return [] if self.value.blank?

type_hash = SiteSetting.type_supervisor.type_hash(self.name.to_sym)
return [] unless type_hash[:schema]&.dig(:properties)

begin
parsed_value = JSON.parse(self.value)
parsed_value = [parsed_value] unless parsed_value.is_a?(Array)
upload_ids = Set.new

parsed_value.each do |obj|
validator = SchemaSettingsObjectValidator.new(schema: type_hash[:schema], object: obj)
upload_ids.merge(validator.property_values_of_type("upload"))
end

upload_ids.to_a
rescue JSON::ParserError
[]
end
end
end

# == Schema Information
Expand Down
5 changes: 4 additions & 1 deletion frontend/discourse/admin/components/schema-setting/field.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import GroupsField from "discourse/admin/components/schema-setting/types/groups"
import IntegerField from "discourse/admin/components/schema-setting/types/integer";
import StringField from "discourse/admin/components/schema-setting/types/string";
import TagsField from "discourse/admin/components/schema-setting/types/tags";
import UploadField from "discourse/admin/components/schema-setting/types/upload";

export default class SchemaSettingField extends Component {
get component() {
const type = this.args.spec.type;

switch (this.args.spec.type) {
switch (type) {
case "string":
return StringField;
case "integer":
Expand All @@ -31,6 +32,8 @@ export default class SchemaSettingField extends Component {
return TagsField;
case "groups":
return GroupsField;
case "upload":
return UploadField;
default:
throw new Error(`unknown type ${type}`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { concat, hash } from "@ember/helper";
import { action } from "@ember/object";
import FieldInputDescription from "discourse/admin/components/schema-setting/field-input-description";
import UppyImageUploader from "discourse/components/uppy-image-uploader";

export default class SchemaSettingTypeUpload extends Component {
@tracked uploadUrl = this.args.value;

@action
uploadDone(upload) {
this.uploadUrl = upload.url;
this.args.onChange(upload.url);
}

@action
uploadDeleted() {
this.uploadUrl = null;
this.args.onChange(null);
}

<template>
<UppyImageUploader
@imageUrl={{this.uploadUrl}}
@onUploadDone={{this.uploadDone}}
@onUploadDeleted={{this.uploadDeleted}}
@additionalParams={{hash for_site_setting=true}}
@type="site_setting"
@id={{concat "schema-field-upload-" @setting.setting}}
@allowVideo={{true}}
/>

{{#if @description}}
<div class="schema-field__input-supporting-text">
<FieldInputDescription @description={{@description}} />
</div>
{{/if}}
</template>
}
15 changes: 14 additions & 1 deletion lib/schema_settings_object_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,21 @@ def has_valid_property_value_type?(property_attributes, property_name)
case type
when "string"
value.is_a?(String)
when "integer", "topic", "post", "upload"
when "integer", "topic", "post"
value.is_a?(Integer)
when "upload"
# Convert upload URLs to IDs like core does
if value.is_a?(String)
upload = Upload.get_from_url(value)
if upload
@object[property_name.to_s] = upload.id
true
else
false
end
else
value.is_a?(Integer)
end
when "float"
value.is_a?(Float) || value.is_a?(Integer)
when "boolean"
Expand Down
52 changes: 51 additions & 1 deletion lib/site_setting_extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,13 @@ def all_settings(
default = default_uploads[default.to_i]
end

# For uploads nested in objects type, hydrate upload IDs to URLs
if type_hash[:type].to_s == "objects" && type_hash[:schema]
parsed_value = JSON.parse(value)

value = hydrate_uploads_in_objects(parsed_value, type_hash[:schema])
end

opts = {
setting: s,
humanized_name: humanized_names(s),
Expand All @@ -400,9 +407,17 @@ def all_settings(
}

if !basic_attributes
# For objects type, serialize as JSON
serialized_value =
if type_hash[:type].to_s == "objects"
value.to_json
else
value.to_s
end

opts.merge!(
default: default,
value: value.to_s,
value: serialized_value,
preview: previews[s],
secret: secret_settings.include?(s),
placeholder: placeholder(s),
Expand Down Expand Up @@ -1125,4 +1140,39 @@ def clear_uploads_cache(name)
def logger
Rails.logger
end

private

def hydrate_uploads_in_objects(objects, schema)
return objects if objects.blank?

all_upload_ids = Set.new
objects.each do |object|
validator = SchemaSettingsObjectValidator.new(schema: schema, object: object)
all_upload_ids.merge(validator.property_values_of_type("upload"))
end

uploads_by_id = Upload.where(id: all_upload_ids.to_a).index_by(&:id)

objects.map { |obj| hydrate_uploads_in_object(obj, schema[:properties], uploads_by_id) }
end

def hydrate_uploads_in_object(object, properties, uploads_by_id)
properties.each do |prop_key, prop_value|
case prop_value[:type]
when "upload"
key = prop_key.to_s
upload_id = object[key]
upload = uploads_by_id[upload_id]
object[key] = upload.url if upload
when "objects"
nested_objects = object[prop_key.to_s]
if nested_objects.is_a?(Array)
nested_objects.each do |nested_obj|
hydrate_uploads_in_object(nested_obj, prop_value[:schema][:properties], uploads_by_id)
end
end
end
end
end
end
69 changes: 69 additions & 0 deletions spec/lib/site_setting_extension_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,75 @@ def self.translate_names?
end
end

describe "objects settings with uploads" do
it "should hydrate upload IDs to URLs" do
upload1 = Fabricate(:upload)
upload2 = Fabricate(:upload)

schema = {
name: "section",
properties: {
title: {
type: "string",
},
image: {
type: "upload",
},
},
}

settings.setting(:test_objects_with_uploads, "[]", type: :objects, schema: schema)
settings.test_objects_with_uploads = [
{ "title" => "Section 1", "image" => upload1.id },
{ "title" => "Section 2", "image" => upload2.id },
].to_json
settings.refresh!

setting = settings.all_settings.last
value = JSON.parse(setting[:value])

expect(value[0]["image"]).to eq(upload1.url)
expect(value[1]["image"]).to eq(upload2.url)
expect(value[0]["title"]).to eq("Section 1")
expect(value[1]["title"]).to eq("Section 2")
end

it "should batch uploads query" do
upload1 = Fabricate(:upload)
upload2 = Fabricate(:upload)
upload3 = Fabricate(:upload)

schema = {
name: "section",
properties: {
title: {
type: "string",
},
image: {
type: "upload",
},
},
}

settings.setting(:test_objects_with_uploads, "[]", type: :objects, schema: schema)
settings.test_objects_with_uploads = [
{ "title" => "Section 1", "image" => upload1.id },
{ "title" => "Section 2", "image" => upload2.id },
{ "title" => "Section 3", "image" => upload3.id },
].to_json
settings.refresh!

queries =
track_sql_queries do
setting = settings.all_settings.last
JSON.parse(setting[:value])
end

upload_queries = queries.select { |q| q.include?("SELECT") && q.include?("uploads") }
expect(upload_queries.length).to eq(1)
end
end

context "with the filter_allowed_hidden argument" do
it "includes the specified hidden settings only if include_hidden is true" do
result =
Expand Down
Loading