hobo_files/plugin/lib/hobo/model.rb in hobo-0.6.2 vs hobo_files/plugin/lib/hobo/model.rb in hobo-0.6.3

- old
+ new

@@ -1,37 +1,32 @@ module Hobo module Model - Hobo.field_types.update({ :html => HtmlString, - :markdown => MarkdownString, - :textile => TextileString, - :password => PasswordString, - :text => Hobo::Text, - :boolean => TrueClass, - :date => Date, - :datetime => Time, - :integer => Fixnum, - :big_integer => BigDecimal, - :float => Float, - :string => String, - :email_address => EmailAddress - }) + PLAIN_TYPES = { :boolean => TrueClass, + :date => Date, + :datetime => Time, + :integer => Fixnum, + :big_integer => BigDecimal, + :float => Float, + :string => String + } + Hobo.field_types.update(PLAIN_TYPES) + def self.included(base) Hobo.register_model(base) base.extend(ClassMethods) base.class_eval do @field_specs = HashWithIndifferentAccess.new set_field_type({}) + alias_method_chain :attributes=, :hobo_type_conversion end class << base alias_method_chain :has_many, :defined_scopes alias_method_chain :belongs_to, :foreign_key_declaration end - # respond_to? is slow on AR objects, use this instead where possible - base.send(:alias_method, :has_hobo_method?, :respond_to_without_attributes?) end module ClassMethods # include methods also shared by CompositeModel @@ -49,54 +44,31 @@ @next_method_type = nil end end - class FieldDeclarationsDsl - - def initialize(model) - @model = model + def fields(&b) + dsl = FieldDeclarationsDsl.new(self) + if b.arity == 1 + yield dsl + else + dsl.instance_eval(&b) end - - attr_reader :model - - def timestamps - field(:created_at, :datetime) - field(:updated_at, :datetime) - end - - def field(name, *args) - type = args.shift - options = args.extract_options! - @model.send(:set_field_type, name => type) unless - type.in?(@model.connection.native_database_types.keys - [:text]) - @model.field_specs[name] = FieldSpec.new(@model, name, type, options) - - @model.send(:validates_presence_of, name) if :required.in?(args) - @model.send(:validates_uniqueness_of, name) if :unique.in?(args) - end - - def method_missing(name, *args) - field(name, *args) - end - end - def fields(&b) - FieldDeclarationsDsl.new(self).instance_eval(&b) - end - - def belongs_to_with_foreign_key_declaration(name, *args, &block) - res = belongs_to_without_foreign_key_declaration(name, *args, &block) + options = args.extract_options! + res = belongs_to_without_foreign_key_declaration(name, *args + [options], &block) refl = reflections[name] fkey = refl.primary_key_name - field_specs[fkey] ||= FieldSpec.new(self, fkey, :integer) + column_options = {} + column_options[:null] = options[:null] if options.has_key?(:null) + field_specs[fkey] ||= FieldSpec.new(self, fkey, :integer, column_options) if refl.options[:polymorphic] type_col = "#{name}_type" - field_specs[type_col] ||= FieldSpec.new(self, type_col, :string) + field_specs[type_col] ||= FieldSpec.new(self, type_col, :string, column_options) end res end @@ -106,13 +78,13 @@ def set_field_type(types) types.each_pair do |field, type| type_class = Hobo.field_types[type] || type field_types[field] = type_class - if "validate".in?(type_class.instance_methods) + if type_class && "validate".in?(type_class.instance_methods) self.validate do |record| - v = record.send(field).validate + v = record.send(field)._?.validate record.errors.add(field, v) if v.is_a?(String) end end end end @@ -134,11 +106,11 @@ @hobo_never_show ||= [] @hobo_never_show.concat(fields.every(:to_sym)) end def never_show?(field) - @hobo_never_show and field.to_sym.in?(@hobo_never_show) + @hobo_never_show && field.to_sym.in?(@hobo_never_show) end public :never_show? def set_creator_attr(attr) @creator_attr = attr.to_sym @@ -174,24 +146,24 @@ def id_name(underscore=false) #{id_name_field} end } end - + key = "id_name#{if underscore; ".gsub('_', ' ')"; end}" finder = if insenstive - "find(:first, :conditions => ['lower(#{id_name_field}) = ?', #{key}.downcase])" + "find(:first, options.merge(:conditions => ['lower(#{id_name_field}) = ?', #{key}.downcase]))" else - "find_by_#{id_name_field}(#{key})" + "find_by_#{id_name_field}(#{key}, options)" end class_eval %{ - def self.find_by_id_name(id_name) + def self.find_by_id_name(id_name, options) #{finder} end } - + model = self validate do erros.add id_name_field, "is taken" if model.find_by_id_name(name) end validates_format_of id_name_field, :with => /^[^_]+$/, :message => "cannot contain underscores" if @@ -205,16 +177,15 @@ end attr_reader :id_name_column - def field_type(name) name = name.to_sym field_types[name] or reflections[name] or begin - col = columns.find {|c| c.name == name.to_s} rescue nil + col = column(name) return nil if col.nil? case col.type when :boolean TrueClass when :text @@ -222,23 +193,22 @@ else col.klass end end end - - def nilable_field?(name) - col = columns.find {|c| c.name == name.to_s} rescue nil - col.nil? || col.null + + def column(name) + columns.find {|c| c.name == name.to_s} rescue nil end def conditions(*args, &b) if args.empty? - ModelQueries.new(self).instance_eval(&b).to_sql + ModelQueries.new(self).instance_eval(&b)._?.to_sql else - ModelQueries.new(self).instance_exec(*args, &b).to_sql + ModelQueries.new(self).instance_exec(*args, &b)._?.to_sql end end def find(*args, &b) @@ -249,23 +219,35 @@ else options.merge(:order => "#{table_name}.#{default_order}") end end - if b && !(block_conditions = conditions(&b)).blank? - c = if !options[:conditions].blank? - "(#{options[:conditons]}) and (#{block_conditions})" + res = if b && !(block_conditions = conditions(&b)).blank? + c = if !options[:conditions].blank? + "(#{sanitize_sql options[:conditions]}) AND (#{sanitize_sql block_conditions})" + else + block_conditions + end + super(args.first, options.merge(:conditions => c)) else - block_conditions + super(*args + [options]) end - super(args.first, options.merge(:conditions => c)) - else - super(*args + [options]) + if args.first == :all + def res.member_class + @member_class + end + res.instance_variable_set("@member_class", self) end + res end + def all(options={}) + find(:all, options.reverse_merge(:order => :default)) + end + + def count(*args, &b) if b sql = ModelQueries.new(self).instance_eval(&b).to_sql options = extract_options_from_args!(args) super(*args + [options.merge(:conditions => sql)]) @@ -312,40 +294,61 @@ end end class ScopedProxy - def initialize(klass, scope={}) + def initialize(klass, scope) @klass = klass # If there's no :find, or :create specified, assume it's a find scope @scope = if scope.has_key?(:find) || scope.has_key?(:create) scope else { :find => scope } end end + def method_missing(name, *args, &block) - @klass.send(:with_scope, @scope) do - @klass.send(name, *args, &block) + if name.to_sym.in?(@klass.defined_scopes.keys) + proxy = @klass.send(name, *args) + proxy.instance_variable_set("@parent_scope", self) + proxy + else + _apply_scope { @klass.send(name, *args, &block) } end end def all self.find(:all) end def first self.find(:first) end + + def member_class + @klass + end + + private + def _apply_scope + if @parent_scope + @parent_scope.send(:_apply_scope) do + @scope ? @klass.send(:with_scope, @scope) { yield } : yield + end + else + @scope ? @klass.send(:with_scope, @scope) { yield } : yield + end + end + end (Object.instance_methods + Object.private_instance_methods + Object.protected_instance_methods).each do |m| ScopedProxy.send(:undef_method, m) unless - m.in?(%w{initialize method_missing send}) || m.starts_with?('_') + m.in?(%w{initialize method_missing send instance_variable_set instance_variable_get puts}) || m.starts_with?('_') end attr_accessor :defined_scopes @@ -382,22 +385,24 @@ # get loaded, hence this trick assoc = Kernel.instance_method(:instance_variable_get).bind(self).call("@#{name}_scope") unless assoc options = proxy_reflection.options - has_many_conditions = options.has_key?(:conditions) + has_many_conditions = options[:conditions] + has_many_conditions = nil if has_many_conditions.blank? source = proxy_reflection.source_reflection scope_conditions = find_scope[:conditions] + scope_conditions = nil if scope_conditions.blank? conditions = if has_many_conditions && scope_conditions - "(#{scope_conditions}) AND (#{has_many_conditions})" + "(#{sanitize_sql scope_conditions}) AND (#{sanitize_sql has_many_conditions})" else scope_conditions || has_many_conditions end - options = options.merge(find_scope).update(:conditions => conditions, - :class_name => proxy_reflection.klass.name, + options = options.merge(find_scope).update(:class_name => proxy_reflection.klass.name, :foreign_key => proxy_reflection.primary_key_name) + options[:conditions] = conditions unless conditions.blank? options[:source] = source.name if source r = ActiveRecord::Reflection::AssociationReflection.new(:has_many, name, options, @@ -435,10 +440,17 @@ end end end + def attributes_with_hobo_type_conversion=(attributes) + converted = attributes.map_hash { |k, v| convert_type_for_mass_assignment(self.class.field_type(k), v) } + self.attributes_without_hobo_type_conversion = converted + end + + + def set_creator(user) self.send("#{self.class.creator_attr}=", user) if (t = self.class.creator_type) && user.is_a?(t) end @@ -458,15 +470,18 @@ res end def same_fields?(other, *fields) + fields = fields.flatten fields.all?{|f| self.send(f) == other.send(f)} end - def changed_fields?(other, *fields) - fields.all?{|f| self.send(f) != other.send(f)} + def only_changed_fields?(other, *changed_fields) + changed_fields = changed_fields.flatten.every(:to_s) + all_cols = self.class.columns.every(:name) - [] + all_cols.all?{|c| c.in?(changed_fields) || self.send(c) == other.send(c) } end def compose_with(object, use=nil) CompositeModel.new_for([self, object]) end @@ -491,10 +506,46 @@ else "#{self.class.name.humanize} #{id}" end end + private + + def parse_datetime(s) + defined?(Chronic) ? Chronic.parse(s) : Time.parse(s) + end + + def convert_type_for_mass_assignment(field_type, value) + if field_type.is_a?(ActiveRecord::Reflection::AssociationReflection) and field_type.macro.in?([:belongs_to, :has_one]) + if value.is_a?(String) && value.starts_with?('@') + Hobo.object_from_dom_id(value[1..-1]) + else + value + end + elsif !field_type.is_a?(Class) + value + elsif field_type <= Date + if value.is_a? Hash + Date.new(*(%w{year month day}.map{|s| value[s].to_i})) + elsif value.is_a? String + dt = parse_datetime(value) + dt && dt.to_date + end + elsif field_type <= Time + if value.is_a? Hash + Time.local(*(%w{year month day hour minute}.map{|s| value[s].to_i})) + elsif value.is_a? String + parse_datetime(value) + end + elsif field_type <= TrueClass + (value.is_a?(String) && value.strip.downcase.in?(['0', 'false']) || value.blank?) ? false : true + else + # primitive field + value + end + end + end end # Hack AR to get Hobo type wrappers in @@ -505,24 +556,42 @@ def define_read_method(symbol, attr_name, column) cast_code = column.type_cast_code('v') if column access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" unless attr_name.to_s == self.primary_key.to_s - access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) " + + "unless @attributes.has_key?('#{attr_name}'); ") end # This is the Hobo hook - add a type wrapper around the field # value if we have a special type defined src = if connected? && respond_to?(:field_type) && (type_wrapper = field_type(symbol)) && - type_wrapper.is_a?(Class) && type_wrapper < String + type_wrapper.is_a?(Class) && type_wrapper.not_in?(Hobo::Model::PLAIN_TYPES.values) "val = begin; #{access_code}; end; " + - "if val.nil?; nil; " + - "elsif val.respond_to?(:hobo_undefined?) && val.hobo_undefined?; val; " + - "else; #{type_wrapper}.new(val); end" + "if val.nil? || (val.respond_to?(:hobo_undefined?) && val.hobo_undefined?); val; " + + "else; self.class.field_type(:#{attr_name}).new(val); end" else access_code end evaluate_attribute_method(attr_name, "def #{symbol}; @attributes_cache['#{attr_name}'] ||= begin; #{src}; end; end") end + + def define_write_method(attr_name) + src = if connected? && respond_to?(:field_type) && (type_wrapper = field_type(attr_name)) && + type_wrapper.is_a?(Class) && type_wrapper.not_in?(Hobo::Model::PLAIN_TYPES.values) + "if val.nil? || (val.respond_to?(:hobo_undefined?) && val.hobo_undefined?); val; " + + "else; self.class.field_type(:#{attr_name}).new(val); end" + else + "val" + end + evaluate_attribute_method(attr_name, "def #{attr_name}=(val); " + + "write_attribute('#{attr_name}', #{src});end", "#{attr_name}=") + + end + +end + +class ActiveRecord::Base + alias_method :has_hobo_method?, :respond_to_without_attributes? end