Class: ImageProfile
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- ImageProfile
- Includes:
- Models::Auditable
- Defined in:
- app/models/image_profile.rb
Overview
== Schema Information
Table name: image_profiles
Database name: primary
id :bigint not null, primary key
image_type :string
locale :string(5) default("en"), not null
transform_params :jsonb
created_at :datetime
updated_at :datetime
creator_id :integer
image_id :integer not null
item_id :integer not null
updater_id :integer
Indexes
index_image_profiles_on_category_item_image_locale ((\nCASE\n WHEN ((image_type)::text ~~ 'WYS_I%'::text) THEN 'WYS_I'::text\n WHEN ((image_type)::text = 'WYS_LIFESTYLE'::text) THEN 'WYS_LIFESTYLE'::text\n WHEN ((image_type)::text ~~ 'WYS_L%'::text) THEN 'WYS_L'::text\n WHEN ((image_type)::text = 'WYS_CARD'::text) THEN 'WYS_CARD'::text\n WHEN ((image_type)::text = 'WYS_MAIN'::text) THEN 'WYS_MAIN'::text\n ELSE "left"((image_type)::text, 3)\nEND), item_id, image_id, locale) UNIQUE
index_image_profiles_on_image_id (image_id)
index_image_profiles_on_image_type (image_type)
index_image_profiles_on_item_id_and_image_type_and_locale (item_id,image_type,locale) UNIQUE
Foreign Keys
fk_rails_... (item_id => items.id) ON DELETE => cascade
Constant Summary collapse
- IMAGE_TYPES =
For Image Type, ensure that the first three characters are unique to a group, e.g AMZ for Amazon, WAL for Walmart
{ AMZ_MAIN: 'Amazon Main image, the primary image on your products detail page', AMZ_FRNT: 'Amazon Front angle shot', AMZ_SIDE: 'Amazon Side angle shot', AMZ_BACK: 'Amazon Back angle shot', AMZ_PT01: 'Amazon Part shot 1, additional angles, product in use, screen shots, accessories, or product details', AMZ_PT02: 'Amazon Part shot 2, additional angles, product in use, screen shots, accessories, or product details', AMZ_PT03: 'Amazon Part shot 3, additional angles, product in use, screen shots, accessories, or product details', AMZ_PT04: 'Amazon Part shot 4, additional angles, product in use, screen shots, accessories, or product details', AMZ_PT05: 'Amazon Part shot 5, additional angles, product in use, screen shots, accessories, or product details', AMZ_PT06: 'Amazon Part shot 6, additional angles, product in use, screen shots, accessories, or product details', AMZ_PT07: 'Amazon Part shot 7, additional angles, product in use, screen shots, accessories, or product details', AMZ_PT08: 'Amazon Part shot 8, additional angles, product in use, screen shots, accessories, or product details', AMZ_PT09: 'Amazon Part shot 9, additional angles, product in use, screen shots, accessories, or product details', AMZ_PT10: 'Amazon Part shot 10, additional angles, product in use, screen shots, accessories, or product details', AMZ_PT11: 'Amazon Part shot 11, additional angles, product in use, screen shots, accessories, or product details', AMZ_SWCH: 'Amazon Swatch shots, shows up in the thumbnail underneath or to the right of the larger image on a detail page. Usually a color sample', WAL_MAIN: 'Walmart Marketplaces Main image of the item', WAL_AD01: 'Walmart Marketplaces Additional image 1', WAL_AD02: 'Walmart Marketplaces Additional image 2', WAL_AD03: 'Walmart Marketplaces Additional image 3', WAL_AD04: 'Walmart Marketplaces Additional image 4', WAL_AD05: 'Walmart Marketplaces Additional image 5', WAL_AD06: 'Walmart Marketplaces Additional image 6', WAL_AD07: 'Walmart Marketplaces Additional image 7', WAL_AD08: 'Walmart Marketplaces Additional image 8', WAL_AD09: 'Walmart Marketplaces Additional image 9', WAL_AD10: 'Walmart Marketplaces Additional image 10', WAL_SWCH: 'Walmart Marketplaces Swatch shot', # WarmlyYours Website image types (WYS = WarmlyYours Site) WYS_CARD: 'WarmlyYours.com Product card / listing thumbnail (not used on PDP carousel or merchant feeds)', WYS_MAIN: 'WarmlyYours.com Main product image', WYS_LIFESTYLE: 'WarmlyYours.com Lifestyle / in-situ room scene image (detail page gallery, after main)', WYS_I01: 'WarmlyYours.com Image 1', WYS_I02: 'WarmlyYours.com Image 2', WYS_I03: 'WarmlyYours.com Image 3', WYS_I04: 'WarmlyYours.com Image 4', WYS_I05: 'WarmlyYours.com Image 5', WYS_I06: 'WarmlyYours.com Image 6', WYS_I07: 'WarmlyYours.com Image 7', WYS_I08: 'WarmlyYours.com Image 8', WYS_I09: 'WarmlyYours.com Image 9', WYS_I10: 'WarmlyYours.com Image 10', WYS_I11: 'WarmlyYours.com Image 11', WYS_I12: 'WarmlyYours.com Image 12', WYS_I13: 'WarmlyYours.com Image 13', WYS_I14: 'WarmlyYours.com Image 14', WYS_I15: 'WarmlyYours.com Image 15', # Wayfair image types (WAY = Wayfair). Optional overrides — when an item has no # WAY_* profiles the Wayfair media push defaults to the website (WYS_*) set. # See Edi::Wayfair::CatalogItemMediaSender#media_for. WAY_MAIN: 'Wayfair Main product image (PDP hero / lead image)', WAY_LIFESTYLE: 'Wayfair Lifestyle / in-situ room scene image', WAY_I01: 'Wayfair Image 1', WAY_I02: 'Wayfair Image 2', WAY_I03: 'Wayfair Image 3', WAY_I04: 'Wayfair Image 4', WAY_I05: 'Wayfair Image 5', WAY_I06: 'Wayfair Image 6', WAY_I07: 'Wayfair Image 7', WAY_I08: 'Wayfair Image 8', WAY_I09: 'Wayfair Image 9', WAY_I10: 'Wayfair Image 10', WAY_I11: 'Wayfair Image 11', WAY_I12: 'Wayfair Image 12', WAY_I13: 'Wayfair Image 13', WAY_I14: 'Wayfair Image 14', WAY_I15: 'Wayfair Image 15' }.freeze
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
-
#image_type ⇒ Object
readonly
The image type must be specified, you can only define one image type per item/locale.
Belongs to collapse
-
#image ⇒ Image
The image this profile attaches to an item.
-
#item ⇒ Item
The item the image is attached to.
Methods included from Models::Auditable
Class Method Summary collapse
-
.amazon_image_profiles ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are amazon image profiles.
-
.image_order ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are image order.
-
.image_types_for_select ⇒ Array<Array(String, String)>
[label, value]pairs for image type select dropdowns. -
.ordered_image_types_for_group(prefix) ⇒ Array<String>
Returns an ordered list of image types for a specific group prefix.
-
.shift_images_for_all_items(locale: 'en', prefix: nil, batch_size: 100, dry_run: false) ⇒ Hash
Shifts images for all items that have image profiles.
-
.shift_images_for_item(item_id, locale: 'en', prefix: nil) ⇒ Object
Shifts images up to fill empty slots in the image type order for a specific item and locale This method will move images to fill gaps in the sequence, maintaining the proper order.
-
.walmart_image_profiles ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are walmart image profiles.
-
.wayfair_image_profiles ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are wayfair image profiles.
-
.website_image_profiles ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are website image profiles.
-
.website_image_profiles_excluding_card ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are website image profiles excluding card.
Instance Method Summary collapse
-
#amazon_image_type ⇒ String?
Amazon image type name with the
AMZ_prefix stripped. -
#amazon_image_type? ⇒ Boolean
Whether this is an Amazon marketplace image type (
AMZ_prefix). -
#effective_transform_params(options = {}) ⇒ Hash
Returns the transform params in effect, with marketplace defaults merged in.
-
#file_name_for_amazon(item) ⇒ String
File name to use when pushing this image to Amazon.
-
#image_url(options = {}) ⇒ String
Generates a proper thumbnail url for the image for import to amazon.
-
#thumbnail_url ⇒ String
Thumbnail URL sized for the Amazon API import.
-
#walmart_marketplace_image_type? ⇒ Boolean
Whether this is a Walmart marketplace image type (
WAL_prefix). -
#website_image_type? ⇒ Boolean
Whether this is a website image type (
WYS_prefix).
Methods included from Models::Auditable
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Models::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#image_type ⇒ Object (readonly)
The image type must be specified, you can only define one image type per item/locale
Validations:
- Presence
- Inclusion ({ in: IMAGE_TYPES.keys.map(&:to_s) + ['TEMPORARY'] })
- Uniqueness ({ scope: %i[item_id locale], unless: :skip_uniqueness_validation })
183 184 185 |
# File 'app/models/image_profile.rb', line 183 validates :image_type, presence: true, inclusion: { in: IMAGE_TYPES.keys.map(&:to_s) + ['TEMPORARY'] }, uniqueness: { scope: %i[item_id locale], unless: :skip_uniqueness_validation } |
Class Method Details
.amazon_image_profiles ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are amazon image profiles. Active Record Scope
165 |
# File 'app/models/image_profile.rb', line 165 scope :amazon_image_profiles, -> { where("image_type LIKE 'AMZ_%'") } |
.image_order ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are image order. Active Record Scope
178 |
# File 'app/models/image_profile.rb', line 178 scope :image_order, -> { where(image_type: IMAGE_TYPES.keys).in_order_of(:image_type, IMAGE_TYPES.keys) } |
.image_types_for_select ⇒ Array<Array(String, String)>
[label, value] pairs for image type select dropdowns.
192 193 194 |
# File 'app/models/image_profile.rb', line 192 def self.image_types_for_select IMAGE_TYPES.map { |k, v| ["#{k} - #{v}", k.to_s] } end |
.ordered_image_types_for_group(prefix) ⇒ Array<String>
Returns an ordered list of image types for a specific group prefix
268 269 270 |
# File 'app/models/image_profile.rb', line 268 def self.ordered_image_types_for_group(prefix) IMAGE_TYPES.keys.select { |type| type.to_s.starts_with?(prefix) }.map(&:to_s) end |
.shift_images_for_all_items(locale: 'en', prefix: nil, batch_size: 100, dry_run: false) ⇒ Hash
Shifts images for all items that have image profiles
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'app/models/image_profile.rb', line 278 def self.shift_images_for_all_items(locale: 'en', prefix: nil, batch_size: 100, dry_run: false) results = { processed_items: 0, skipped_items: 0, errors: [], dry_run: dry_run } # Find all items that have image profiles items_with_profiles = Item.joins(:image_profiles) .where(image_profiles: { locale: locale }) .distinct # Apply prefix filter if specified if prefix.present? prefix = prefix.upcase items_with_profiles = items_with_profiles.where('image_profiles.image_type LIKE ?', "#{prefix}%") end total_items = items_with_profiles.count puts "Found #{total_items} items with image profiles#{" for prefix #{prefix}" if prefix} in locale #{locale}" puts "Dry run mode: #{dry_run ? 'ON' : 'OFF'}" puts '=' * 60 items_with_profiles.find_in_batches(batch_size: batch_size) do |batch| batch.each do |item| begin puts "Processing item #{item.id} (#{item.sku}): #{item.name}" # Show before state profiles_query = item.image_profiles.where(locale: locale) profiles_query = profiles_query.where('image_type LIKE ?', "#{prefix}%") if prefix.present? before_profiles = profiles_query.order(:image_type).pluck(:image_type) puts " Before: #{before_profiles.join(', ')}" if dry_run puts ' [DRY RUN] Would process this item' else # Perform the shift shift_images_for_item(item.id, locale: locale, prefix: prefix) # Show after state profiles_query = item.image_profiles.where(locale: locale) profiles_query = profiles_query.where('image_type LIKE ?', "#{prefix}%") if prefix.present? after_profiles = profiles_query.order(:image_type).pluck(:image_type) puts " After: #{after_profiles.join(', ')}" # Check if any changes were made if before_profiles == after_profiles puts ' - No changes needed' else puts ' ✓ Changes made' end end results[:processed_items] += 1 rescue StandardError => e error_msg = "Error processing item #{item.id} (#{item.sku}): #{e.}" puts " ✗ #{error_msg}" results[:errors] << error_msg end puts '' end end # Print summary puts '=' * 60 puts 'SUMMARY:' puts " Processed items: #{results[:processed_items]}" puts " Errors: #{results[:errors].count}" puts " Dry run: #{results[:dry_run]}" if results[:errors].any? puts "\nERRORS:" results[:errors].each { |error| puts " - #{error}" } end results end |
.shift_images_for_item(item_id, locale: 'en', prefix: nil) ⇒ Object
Shifts images up to fill empty slots in the image type order for a specific item and locale
This method will move images to fill gaps in the sequence, maintaining the proper order
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 |
# File 'app/models/image_profile.rb', line 201 def self.shift_images_for_item(item_id, locale: 'en', prefix: nil) transaction do # Get all image profiles for this item and locale profiles = where(item_id: item_id, locale: locale).order(:image_type) # Group profiles by their image type prefix (first 3 characters) grouped_profiles = profiles.group_by { |profile| profile.image_type.to_s[0, 3] } # Filter by prefix if specified if prefix.present? prefix = prefix.upcase grouped_profiles = grouped_profiles.select { |group_prefix, _| group_prefix == prefix } end grouped_profiles.each do |group_prefix, group_profiles| # Get the ordered list of image types for this group ordered_types = ordered_image_types_for_group(group_prefix) # Skip if no ordered types found for this group next if ordered_types.empty? # Create a mapping of current image types to their profiles current_mapping = group_profiles.index_by(&:image_type) # Find the first available slot and shift images up available_slots = ordered_types.select { |type| current_mapping[type].nil? } occupied_slots = ordered_types.select { |type| current_mapping[type].present? } # If there are no gaps, no shifting needed next if available_slots.empty? || occupied_slots.empty? new_mapping = {} # Sort by canonical IMAGE_TYPES order so shifts always move images # toward lower slots — avoids unique-constraint collisions during saves. available_images = group_profiles .reject { |profile| profile.image_type.end_with?('_SWCH') } .sort_by { |profile| ordered_types.index(profile.image_type) || 999 } target_positions = ordered_types.reject { |type| type.end_with?('_SWCH') } target_positions.each_with_index do |target_type, index| break unless index < available_images.length new_mapping[target_type] = available_images[index] end changes = new_mapping.reject { |new_type, profile| profile.image_type == new_type } next if changes.empty? # Two-pass update to avoid unique constraint violations when slots swap. # Pass 1: park all moving profiles in unique temporary slots where(id: changes.values.map(&:id)) .update_all(image_type: Arel.sql("'__SHIFT_' || id::text")) # Pass 2: assign final types changes.each do |new_type, profile| where(id: profile.id).update_all(image_type: new_type) profile.image_type = new_type end end end end |
.walmart_image_profiles ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are walmart image profiles. Active Record Scope
166 |
# File 'app/models/image_profile.rb', line 166 scope :walmart_image_profiles, -> { where("image_type LIKE 'WAL_%'") } |
.wayfair_image_profiles ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are wayfair image profiles. Active Record Scope
168 |
# File 'app/models/image_profile.rb', line 168 scope :wayfair_image_profiles, -> { where("image_type LIKE 'WAY_%'") } |
.website_image_profiles ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are website image profiles. Active Record Scope
167 |
# File 'app/models/image_profile.rb', line 167 scope :website_image_profiles, -> { where("image_type LIKE 'WYS_%'") } |
.website_image_profiles_excluding_card ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are website image profiles excluding card. Active Record Scope
170 |
# File 'app/models/image_profile.rb', line 170 scope :website_image_profiles_excluding_card, -> { website_image_profiles.where.not(image_type: 'WYS_CARD') } |
Instance Method Details
#amazon_image_type ⇒ String?
Amazon image type name with the AMZ_ prefix stripped.
369 370 371 |
# File 'app/models/image_profile.rb', line 369 def amazon_image_type image_type.to_s.gsub('AMZ_', '').presence end |
#amazon_image_type? ⇒ Boolean
Whether this is an Amazon marketplace image type (AMZ_ prefix).
363 364 365 |
# File 'app/models/image_profile.rb', line 363 def amazon_image_type? image_type.to_s.starts_with?('AMZ_') end |
#effective_transform_params(options = {}) ⇒ Hash
Returns the transform params in effect, with marketplace defaults merged in.
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 |
# File 'app/models/image_profile.rb', line 409 def effective_transform_params( = {}) # Defaults (which can be overridden by options) # For simplicity, I created a named profile n-amazon_api # { width: 2000, height: 2000, thumbnail: true, encode_format: :jpeg, transformation_position: 'path' } # see https://imagekit.io/dashboard/settings/named-transforms = .reverse_merge({ named: (image_type == 'AMZ_SWCH' ? 'amazon_swatch' : 'amazon_api'), transformation_position: 'path', dpr: :ignore }) if amazon_image_type? # Also created a named profile n-walmart_marketplace_api # { width: 2200, height: 2200, thumbnail: true, encode_format: :jpeg, transformation_position: 'path' } # see https://imagekit.io/dashboard/settings/named-transforms = .reverse_merge({ named: 'walmart_marketplace_api', transformation_position: 'path', dpr: :ignore }) if walmart_marketplace_image_type? # Transform params, non negotiable when set .merge(transform_params.symbolize_keys) end |
#file_name_for_amazon(item) ⇒ String
File name to use when pushing this image to Amazon.
428 429 430 |
# File 'app/models/image_profile.rb', line 428 def file_name_for_amazon(item) "#{item.amazon_asin}.#{image_type}.jpg" end |
#image ⇒ Image
The image this profile attaches to an item.
161 |
# File 'app/models/image_profile.rb', line 161 belongs_to :image |
#image_url(options = {}) ⇒ String
Generates a proper thumbnail url for the image for import to amazon
392 393 394 |
# File 'app/models/image_profile.rb', line 392 def image_url( = {}) image.image_url(**effective_transform_params()) end |
#item ⇒ Item
The item the image is attached to.
163 |
# File 'app/models/image_profile.rb', line 163 belongs_to :item |
#thumbnail_url ⇒ String
Thumbnail URL sized for the Amazon API import.
398 399 400 |
# File 'app/models/image_profile.rb', line 398 def thumbnail_url image_url(named: 'amazon_api_thumb_300') end |
#walmart_marketplace_image_type? ⇒ Boolean
Whether this is a Walmart marketplace image type (WAL_ prefix).
375 376 377 |
# File 'app/models/image_profile.rb', line 375 def walmart_marketplace_image_type? image_type.to_s.starts_with?('WAL_') end |
#website_image_type? ⇒ Boolean
Whether this is a website image type (WYS_ prefix).
381 382 383 |
# File 'app/models/image_profile.rb', line 381 def website_image_type? image_type.to_s.starts_with?('WYS_') end |