module Schofield module Generators class Level COLUMNS_TO_IGNORE = %w( ^created_at$ ^updated_at$ _content_type$ _file_size$ _updated_at$ _type$ ^type$ ^crypted_password$ ^password_salt$ ^persistence_token$ ^perishable_token$ ^login_count$ ^failed_login_count$ ^last_request_at$ ^current_login_at$ ^last_login_at$ ^current_login_ip$ ^last_login_ip$ ) class << self; attr_reader :columns_to_ignore end def self.tables_to_ignore= tables @columns_to_ignore = ( COLUMNS_TO_IGNORE + tables.map{ |m| "^#{m.singularize}_id$" } ) end attr_reader :model, :name, :parent_associations, :attributes, :superclass attr_accessor :child_associations, :subclasses delegate :validations, :validations?, :attr_accessors, :attr_accessors?, :attr_accessibles, :attr_accessibles?, :attachments?, :to_s_string, :attached_files, :to => :attributes def initialize model, superclass=nil @model = model @name = model.name.underscore @human_name = @name.gsub('_', ' ') @superclass = superclass @subclasses = [] @parent_associations = [] @child_associations = [] add_parent_associations add_attributes end # Inheritence def subclass? @superclass.present? end def superclass? @subclasses.any? end # Join table def join? @join ||= @model.columns.select { |c| c.name.match(/_id$/) }.length == 2 && @model.columns.select { |c| !%w( id created_at updated_at position ).include?(c.name) }.length == 2 end def other_parent_name parent_name @parent_associations.find{ |a| a.parent_name != parent_name }.parent_name end # Polymorphism def polymorphic? @polymorphic ||= polymorphic_name.present? end def polymorphic_name match_data = nil @polymorphic_name ||= @model.columns.find { |c| match_data = c.name.match(/^(.+)_type$/) } ? match_data[1] : nil end # Nesting def nested? @parent_associations.any?(&:nest?) end def nests? @child_associations.any?(&:nest?) end def nested_associations if superclass? then [] else associations = @child_associations + ( subclass? ? @superclass.child_associations : [] ) end end def nested_levels nested_associations.select(&:nest?).map(&:child) end # Associations and cardinality def belongs_to? @parent_associations.any? end def belongs_to_one_names @parent_associations.select(&:one_to_one?).map(&:parent_name) end def belongs_to_one? @parent_associations.any?(&:one_to_one?) end def has_ones? @child_associations.any?(&:one_to_one?) end def has_manies? @child_associations.any?(&:one_to_many?) end def has_ones @child_associations.select(&:one_to_one?).map(&:child) end def has_manies @child_associations.select(&:one_to_many?).map(&:child) end # Combos def routes? !nested? && !superclass? && !belongs_to_one? && !join? end def controllers? !superclass? && name != 'user' && !belongs_to_one? end def views? controllers? && !join? end def models? name != 'user' end def tables? !belongs_to_one? && !superclass? end # Sortable def sortable? @sortable end # Form def multipart? attachments? || has_ones.any?(&:attachments?) end # Association ancestry def ancestry level = self nested_ins = [] next_name = nil while level if next_name this_name = next_name next_name = nil else this_name = level.name end nested_ins << this_name next_name = level.polymorphic_name if level.polymorphic? level = level.parent_associations.select(&:nest?).map(&:parent).first # polymorphic model could have more than one parent!!! end nested_ins.reverse end # Form fields def attribute_of_nesting_parent? attribute attribute.model_name && parent_associations.select(&:nest?).map(&:parent_name).include?(attribute.model_name) end def polymorphic_attribute? attribute polymorphic_name && attribute.model_name == polymorphic_name end def form_field? attribute !%w( position slug ).include?(attribute.name) && attribute.name !~ /_fingerprint$/ && !polymorphic_attribute?(attribute) && !attribute_of_nesting_parent?(attribute) end private def add_attributes @attributes = Attributes.new @model.columns.each do |column| add_attribute(column) unless ignore?(column) || (polymorphic? && column.name =~ /^#{polymorphic_name}_id$/) end end def add_attribute column attribute = @attributes.new_attribute(column, belongs_to_one_names.include?(column.name.gsub(/_id$/, ''))) set_sortable if attribute.name == 'position' end def set_sortable unless superclass? @sortable = true Levels.sortable = @name end end # Nesting refers to nested routes, embedding refers to nested_attributes_for # Assuming that a polymorphic model has no parents other than it's polymorphically associated parents # If model has a one-to-one relationship with a parent, no nesting will occur as child will be embedded in parent # Only allowing child to be nested under one parent or if polymorphic, the polymorphically associated parents def add_parent_associations if polymorphic? answer = Responses.get("Which models are #{polymorphic_name}?") answer.split(/[^\w]+/).map(&:underscore).each do |parent_name| @parent_associations << Association.new(self, parent_name, one_to_one?(polymorphic_name), polymorphic_name) end else any_one_to_ones = false @model.columns.each do |column| if (match_data = column.name.match(/^(.+)_id$/)).present? && Levels.exists?(match_data[1]) parent_name = match_data[1] is_one_to_one = one_to_one?(parent_name) any_one_to_ones = true if is_one_to_one @parent_associations << Association.new(self, parent_name, is_one_to_one) end end if @parent_associations.any? && !any_one_to_ones @parent_associations.length == 1 ? ask_if_nested : ask_where_to_nest end end end def one_to_one? parent_name @one_to_one ||= %w( y yes ).include?(Responses.get("#{parent_name.gsub('_', ' ')} HAS ONE #{@human_name}? [N]").downcase) end def ask_if_nested question = "Do you wish to nest #{@human_name} in #{@parent_associations.first.parent_name}? [yes]" @parent_associations.first.nest = true unless %w( n no ).include?(Responses.get(question).downcase.strip) end def ask_where_to_nest question = "Where do you wish to nest #{@human_name}? nowhere(n), " question += @parent_associations.enum_with_index.map{ |n,i| "#{n.parent_name.gsub('_', ' ')}(#{i})" }.join(', ') + ' [n]' answer = Responses.get(question) @parent_associations[answer.to_i].nest = true unless answer == 'n' || answer.blank? end def ignore? column self.class.columns_to_ignore.each do |string| return true if column.name.match(/#{string}/) end column.primary end end end end