require 'active_support/concern' module HasAttributes extend ActiveSupport::Concern included do attr_accessor :custom_attribute_values after_save :update_custom_attributes def self.create_default_attribute_categories(user) # Don't create any attribute categories for AttributeCategories or AttributeFields that share the ContentController return [] if ['attribute_category', 'attribute_field'].include?(content_name) YAML.load_file(Rails.root.join('config', 'attributes', "#{content_name}.yml")).map do |category_name, defaults| # First, query for the category to see if it already exists category = user.attribute_categories.find_or_initialize_by( entity_type: self.content_name, name: category_name.to_s ) creating_new_category = category.new_record? # If the category didn't already exist, go ahead and set defaults on it and save if creating_new_category category.label = defaults[:label] category.icon = defaults[:icon] category.save! end # If we created this category for the first time, we also want to make sure we create its default fields, too if creating_new_category && defaults.key?(:attributes) category.attribute_fields << defaults[:attributes].map do |field| af_field = category.attribute_fields.with_deleted.create!( old_column_source: field[:name], user: user, field_type: field[:field_type].presence || "text_area", label: field[:label].presence || 'Untitled field' ) af_field end end end.compact end def self.attribute_categories(user, show_hidden: false) return [] if ['attribute_category', 'attribute_field'].include?(content_name) # Cache the result in case we call this function multiple times this request @cached_attribute_categories_for_this_content = begin # Always include the flatfile categories (but create AR versions if they don't exist) categories = YAML.load_file(Rails.root.join('config', 'attributes', "#{content_name}.yml")).map do |category_name, details| category = ::AttributeCategory.with_deleted.find_or_initialize_by( entity_type: self.content_name, name: category_name.to_s, user: user ) # Default new categories to some sane defaults unless category.persisted? category.icon = details[:icon] category.label = details[:label] end category.save! if user && category.new_record? category.attribute_fields << details[:attributes].map do |field| af_field = category.attribute_fields.with_deleted.find_or_initialize_by( # label: field[:label], old_column_source: field[:name], user: user, field_type: field[:field_type].presence || "text_area" ) if af_field.label.nil? af_field.label = field[:label] end if user && af_field.new_record? af_field.save! end af_field end if details.key?(:attributes) if show_hidden category else !!category.hidden ? nil : category end end.compact if categories.first&.user&.present? acceptable_hidden_values = show_hidden ? [true, false, nil] : [false, nil] categories .first .user .attribute_categories .where(entity_type: self.content_name, hidden: acceptable_hidden_values) .eager_load(attribute_fields: :attribute_values) .order('attribute_categories.position, attribute_categories.created_at, attribute_categories.id') # We need to do something like this, but... not this. #.eager_load(attribute_fields: :attribute_values) #.eager_load(:attribute_fields) else categories end end @cached_attribute_categories_for_this_content end # Replacement method for the above that doesn't make create calls, for use after everyone is migrated # def self.attribute_categories(user, show_hidden: false) # return [] if ['attribute_category', 'attribute_field'].include?(content_name) # # This doesn't work the way I think it does # #return @cached_attribute_categories_for_this_content if @cached_attribute_categories_for_this_content # # Always include the flatfile categories (but create AR versions if they don't exist) # categories = YAML.load_file(Rails.root.join('config', 'attributes', "#{content_name}.yml")).map do |category_name, details| # category = ::AttributeCategory.with_deleted.find_by( # entity_type: self.content_name, # name: category_name.to_s, # user: user # ) # # category.attribute_fields << details[:attributes].map do |field| # # category.attribute_fields.with_deleted.find_by( # # user: user, # # old_column_source: field[:name], # # field_type: field[:field_type].presence || "text_area" # # ) # # end if details.key?(:attributes) # if show_hidden # category # else # !!category.hidden ? nil : category # end # end.compact # # Cache the result in case we call this function multiple times this request # @cached_attribute_categories_for_this_content = begin # if categories.first&.user&.present? # acceptable_hidden_values = show_hidden ? [true, false, nil] : [false, nil] # categories # .first # .user # .attribute_categories # .where(entity_type: self.content_name, hidden: acceptable_hidden_values) # .eager_load(attribute_fields: :attribute_values) # .order('attribute_categories.position, attribute_categories.created_at, attribute_categories.id') # # We need to do something like this, but... not this. # #.eager_load(attribute_fields: :attribute_values) # .eager_load(:attribute_fields) # else # categories # end # end # @cached_attribute_categories_for_this_content # end def update_custom_attributes (self.custom_attribute_values || []).each do |attribute| field = AttributeField.includes(:attribute_category).find_by( name: attribute[:name], user: self.user, attribute_categories: { entity_type: self.class.name.downcase } ) #todo why is this commented out? is it needed? #next if field.nil? raise "unknown field for attribute: #{attribute.inspect}" if field.nil? d = field.attribute_values.find_or_initialize_by( attribute_field_id: field.id, entity_type: self.class.name, entity_id: self.id, user: self.user ) if d.value != attribute[:value] || d.new_record? d.value = attribute[:value] d.save! end end end def name_field category_ids = AttributeCategory.where( user_id: user_id, entity_type: self.class.name.downcase ).pluck(:id) # Todo these two queries should be able to be joined into one AttributeField.find_by( user_id: user_id, attribute_category_id: category_ids, field_type: 'name' ) end def universe_field category_ids = AttributeCategory.where( user_id: user_id, entity_type: self.class.name.downcase ).pluck(:id) # Todo these two queries should be able to be joined into one AttributeField.find_by( user_id: user_id, attribute_category_id: category_ids, field_type: 'universe' ) end def overview_field(label) category_ids = AttributeCategory.where( user_id: user_id, entity_type: self.class.name.downcase ).pluck(:id) # Todo these two queries should be able to be joined into one AttributeField.find_by( user_id: user_id, attribute_category_id: category_ids, label: label, hidden: [nil, false] ) end def name_field_value @name_field_lookup_cache ||= {} cache_key = "#{self.class.name}-#{self.id.to_s}" if @name_field_lookup_cache.key?(cache_key) return @name_field_lookup_cache[cache_key] end name_field_cache = name_field if name_field_cache.nil? @name_field_lookup_cache[cache_key] = self.name return self.name end field_value = name_field_cache.attribute_values.detect { |v| v.entity_id == self.id }&.value.presence || self.name.presence || "Untitled #{self.class.name}" @name_field_lookup_cache[cache_key] = field_value field_value end def universe_field_value universe_field_cache = universe_field return nil unless self.respond_to?(:universe_id) return self.universe_id if universe_field_cache.nil? universe_id = universe_field_cache.attribute_values.detect do |v| v.entity_id == self.id end&.value.presence || self.universe_id.presence || nil if universe_id Universe.find_by(id: universe_id.to_i) else nil end end def overview_field_value(label) field_cache = overview_field(label) return nil if field_cache.nil? field_cache.attribute_values.detect { |v| v.entity_id == self.id }&.value.presence || (self.respond_to?(label.downcase) ? self.read_attribute(label.downcase) : nil) end def self.field_type_for(category, field) if field[:label] == 'Name' && category.name == 'overview' "name" elsif field[:label] == 'Universe' && category.name == 'overview' "universe" else "textarea" end end end end