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