Class: ImageProfile
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- ImageProfile
- Includes:
- Models::Auditable, OrderQuery
- 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' }
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
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
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_types_for_select ⇒ Object
-
.ordered_image_types_for_group(prefix) ⇒ Object
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.
-
.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 ⇒ Object
- #amazon_image_type? ⇒ Boolean
- #effective_transform_params(options = {}) ⇒ Object
- #file_name_for_amazon(item) ⇒ Object
-
#image_url(options = {}) ⇒ Object
Generates a proper thumbnail url for the image for import to amazon.
- #thumbnail_url ⇒ Object
- #walmart_marketplace_image_type? ⇒ Boolean
- #website_image_type? ⇒ Boolean
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::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 })
159 160 161 |
# File 'app/models/image_profile.rb', line 159 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
145 |
# File 'app/models/image_profile.rb', line 145 scope :amazon_image_profiles, -> { where("image_type LIKE 'AMZ_%'") } |
.image_types_for_select ⇒ Object
166 167 168 |
# File 'app/models/image_profile.rb', line 166 def self.image_types_for_select IMAGE_TYPES.map { |k, v| ["#{k} - #{v}", k.to_s] } end |
.ordered_image_types_for_group(prefix) ⇒ Object
Returns an ordered list of image types for a specific group prefix
239 240 241 |
# File 'app/models/image_profile.rb', line 239 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
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 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 |
# File 'app/models/image_profile.rb', line 249 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
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 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 |
# File 'app/models/image_profile.rb', line 175 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
146 |
# File 'app/models/image_profile.rb', line 146 scope :walmart_image_profiles, -> { where("image_type LIKE 'WAL_%'") } |
.website_image_profiles ⇒ ActiveRecord::Relation<ImageProfile>
A relation of ImageProfiles that are website image profiles. Active Record Scope
147 |
# File 'app/models/image_profile.rb', line 147 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
149 |
# File 'app/models/image_profile.rb', line 149 scope :website_image_profiles_excluding_card, -> { website_image_profiles.where.not(image_type: 'WYS_CARD') } |
Instance Method Details
#amazon_image_type ⇒ Object
336 337 338 |
# File 'app/models/image_profile.rb', line 336 def amazon_image_type image_type.to_s.gsub('AMZ_', '').presence end |
#amazon_image_type? ⇒ Boolean
332 333 334 |
# File 'app/models/image_profile.rb', line 332 def amazon_image_type? image_type.to_s.starts_with?('AMZ_') end |
#effective_transform_params(options = {}) ⇒ Object
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 |
# File 'app/models/image_profile.rb', line 357 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) ⇒ Object
373 374 375 |
# File 'app/models/image_profile.rb', line 373 def file_name_for_amazon(item) "#{item.amazon_asin}.#{image_type}.jpg" end |
#image_url(options = {}) ⇒ Object
Generates a proper thumbnail url for the image for import to amazon
349 350 351 |
# File 'app/models/image_profile.rb', line 349 def image_url( = {}) image.image_url(**effective_transform_params()) end |
#thumbnail_url ⇒ Object
353 354 355 |
# File 'app/models/image_profile.rb', line 353 def thumbnail_url image_url(named: 'amazon_api_thumb_300') end |
#walmart_marketplace_image_type? ⇒ Boolean
340 341 342 |
# File 'app/models/image_profile.rb', line 340 def walmart_marketplace_image_type? image_type.to_s.starts_with?('WAL_') end |
#website_image_type? ⇒ Boolean
344 345 346 |
# File 'app/models/image_profile.rb', line 344 def website_image_type? image_type.to_s.starts_with?('WYS_') end |