hobo_files/plugin/lib/hobo/model.rb in hobo-0.7.2 vs hobo_files/plugin/lib/hobo/model.rb in hobo-0.7.3

- old
+ new

@@ -1,342 +1,237 @@ module Hobo module Model + class PermissionDeniedError < RuntimeError; end + class NoNameError < RuntimeError; end + NAME_FIELD_GUESS = %w(name title) PRIMARY_CONTENT_GUESS = %w(description body content profile) SEARCH_COLUMNS_GUESS = %w(name title body content profile) - 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) + + included_in_class_callbacks(base) + + Hobo.register_model(base) + + patch_will_paginate + base.class_eval do + inheriting_cattr_reader :default_order alias_method_chain :attributes=, :hobo_type_conversion - default_scopes end + class << base - alias_method_chain :has_many, :defined_scopes - alias_method_chain :belongs_to, :foreign_key_declaration - alias_method_chain :belongs_to, :hobo_metadata - alias_method_chain :acts_as_list, :fields if defined?(ActiveRecord::Acts::List) + alias_method_chain :has_many, :defined_scopes + alias_method_chain :belongs_to, :creator_metadata + + alias_method_chain :has_one, :new_method + def inherited(klass) fields do Hobo.register_model(klass) field(klass.inheritance_column, :string) end end end + end + + def self.patch_will_paginate + if defined?(WillPaginate) && !WillPaginate::Collection.respond_to?(:member_class) + + WillPaginate::Collection.class_eval do + attr_accessor :member_class, :origin, :origin_attribute + end + + WillPaginate::Finder::ClassMethods.class_eval do + def paginate_with_hobo_metadata(*args, &block) + returning paginate_without_hobo_metadata(*args, &block) do |collection| + collection.member_class = self + collection.origin = try.proxy_owner + collection.origin_attribute = try.proxy_reflection._?.name + end + end + alias_method_chain :paginate, :hobo_metadata + + end + + end + end + module ClassMethods # include methods also shared by CompositeModel - include ModelSupport::ClassMethods + #include ModelSupport::ClassMethods attr_accessor :creator_attribute attr_writer :name_attribute, :primary_content_attribute - def default_scopes - def_scope :recent do |*args| - count = args.first || 3 - { :limit => count, :order => "#{table_name}.created_at DESC" } - end + def named(*args) + raise NoNameError, "Model #{name} has no name attribute" unless name_attribute + send("find_by_#{name_attribute}", *args) end + alias_method :[], :named + + + def field_added(name, type, args, options) + self.name_attribute = name.to_sym if options.delete(:name) + self.primary_content_attribute = name.to_sym if options.delete(:primary_content) + self.creator_attribute = name.to_sym if options.delete(:creator) + validate = options.delete(:validate) {true} + + #FIXME - this should be in Hobo::User + send(:login_attribute=, name.to_sym, validate) if options.delete(:login) && respond_to?(:login_attribute=) + end + + + def user_find(user, *args) + record = find(*args) + raise PermissionDeniedError unless Hobo.can_view?(user, self) + record + end + + + def user_new(user, attributes={}) + record = new(attributes) + record.user_changes(user) and record + end + + + def user_new!(user, attributes={}) + user_new(user, attributes) or raise PermissionDeniedError + end + + + def user_create(user, attributes={}) + record = new(attributes) + record.user_save_changes(user) + record + end + + + def user_can_create?(user, attributes={}) + record = new(attributes) + record.user_changes(user) + end + + def name_attribute @name_attribute ||= begin - cols = columns.every :name - NAME_FIELD_GUESS.detect {|f| f.in? columns.every(:name) } + cols = columns.*.name + NAME_FIELD_GUESS.detect {|f| f.in? columns.*.name } end end def primary_content_attribute - @description_attribute ||= begin - cols = columns.every :name - PRIMARY_CONTENT_GUESS.detect {|f| f.in? columns.every(:name) } - end + @primary_content_attribute ||= begin + cols = columns.*.name + PRIMARY_CONTENT_GUESS.detect {|f| f.in? columns.*.name } + end end def dependent_collections reflections.values.select do |refl| refl.macro == :has_many && refl.options[:dependent] - end.every(:name) + end.*.name end def dependent_on reflections.values.select do |refl| refl.macro == :belongs_to && (rev = reverse_reflection(refl.name) and rev.options[:dependent]) - end.every(:name) + end.*.name end - private - def return_type(type) - @next_method_type = type + def default_dependent_on + dependent_on.first end - def method_added(name) - if @next_method_type - set_field_type(name => @next_method_type) - @next_method_type = nil - end - end + private - def fields(&b) - dsl = FieldDeclarationsDsl.new(self) - if b.arity == 1 - yield dsl - else - dsl.instance_eval(&b) - end - end - - - def belongs_to_with_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 - 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, column_options) - end - res - end - - - def belongs_to_with_hobo_metadata(name, *args, &block) - options = args.extract_options! + + def belongs_to_with_creator_metadata(name, options={}, &block) self.creator_attribute = name.to_sym if options.delete(:creator) - belongs_to_without_hobo_metadata(name, *args + [options], &block) + belongs_to_without_creator_metadata(name, options, &block) end - - - def acts_as_list_with_fields(options = {}) - fields { |f| f.send(options._?[:column] || "position", :integer) } - acts_as_list_without_fields(options) - end - - - def field_specs - @field_specs ||= HashWithIndifferentAccess.new - end - public :field_specs - def set_field_type(types) - types.each_pair do |field, type| - type_class = Hobo.field_types[type] || type - field_types[field] = type_class - - if type_class && "validate".in?(type_class.instance_methods) - self.validate do |record| - v = record.send(field)._?.validate - record.errors.add(field, v) if v.is_a?(String) - end - end - end + + def has_one_with_new_method(name, options={}, &block) + has_one_without_new_method(name, options) + class_eval "def new_#{name}(attributes={}); build_#{name}(attributes, false); end" end - def field_types - @hobo_field_types ||= superclass.respond_to?(:field_types) ? superclass.field_types : {} - end - - def set_default_order(order) @default_order = order end - inheriting_attr_accessor :default_order, :id_name_options - def never_show(*fields) @hobo_never_show ||= [] - @hobo_never_show.concat(fields.every(:to_sym)) + @hobo_never_show.concat(fields.*.to_sym) end - def never_show?(field) - (@hobo_never_show && field.to_sym.in?(@hobo_never_show)) || (superclass < Hobo::Model && superclass.never_show?(field)) - end - public :never_show? - + def set_search_columns(*columns) class_eval %{ def self.search_columns - %w{#{columns.every(:to_s) * ' '}} + %w{#{columns.*.to_s * ' '}} end } end - - def id_name(*args) - @id_name_options = [] + args - - underscore = args.delete(:underscore) - insenstive = args.delete(:case_insensitive) - id_name_field = args.first || :name - @id_name_column = id_name_field.to_s - - if underscore - class_eval %{ - def id_name(underscore=false) - underscore ? #{id_name_field}.gsub(' ', '_') : #{id_name_field} - end - } - else - class_eval %{ - def id_name(underscore=false) - #{id_name_field} - end - } - end - - key = "id_name#{if underscore; ".gsub('_', ' ')"; end}" - finder = if insenstive - "find(:first, options.merge(:conditions => ['lower(#{id_name_field}) = ?', #{key}.downcase]))" - else - "find_by_#{id_name_field}(#{key}, options)" - end - - class_eval %{ - 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 - underscore - end - - public - def id_name? - respond_to?(:find_by_id_name) - end + public - attr_reader :id_name_column - - def field_type(name) - name = name.to_sym - field_types[name] or - reflections[name] or begin - col = column(name) - return nil if col.nil? - case col.type - when :boolean - TrueClass - when :text - Hobo::Text - else - col.klass - end - end + def never_show?(field) + (@hobo_never_show && field.to_sym.in?(@hobo_never_show)) || (superclass < Hobo::Model && superclass.never_show?(field)) end - - 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 - else - ModelQueries.new(self).instance_exec(*args, &b)._?.to_sql - end - end - def find(*args, &b) options = args.extract_options! - if args.first.in?([:all, :first]) && options[:order] == :default + if options[:order] == :default options = if default_order.blank? - options - [:order] + options.except :order else options.merge(:order => "#{table_name}.#{default_order}") end end - - 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 - super(*args + [options]) - end - if args.first == :all - def res.member_class - @member_class - end - res.instance_variable_set("@member_class", self) - end - res + result = super(*args + [options]) + result.member_class = self if result.is_a?(Array) + result 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)]) - else - super(*args) - end - end - - - def subclass_associations(association, *subclass_associations) - refl = reflections[association] - for assoc in subclass_associations - class_name = assoc.to_s.classify - options = { :class_name => class_name, :conditions => "type = '#{class_name}'" } - options[:source] = refl.source_reflection.name if refl.source_reflection - has_many(assoc, refl.options.merge(options)) - end - end - def creator_type reflections[creator_attribute]._?.klass end + def search_columns - cols = columns.every(:name) + cols = columns.*.name SEARCH_COLUMNS_GUESS.select{|c| c.in?(cols) } end - # This should really be a method on AssociationReflection + + # FIXME: This should really be a method on AssociationReflection def reverse_reflection(association_name) refl = reflections[association_name] return nil if refl.options[:conditions] reverse_macro = if refl.macro == :has_many @@ -351,173 +246,119 @@ r.primary_key_name == refl.primary_key_name end end - class ScopedProxy - - 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) - 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 - + def has_inheritance_column? + columns_hash.include?(inheritance_column) 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 instance_variable_set instance_variable_get puts}) || m.starts_with?('_') - end - attr_accessor :defined_scopes - - def def_scope(name, scope=nil, &block) - @defined_scopes ||= {} - @defined_scopes[name.to_sym] = block || scope - - meta_def(name) do |*args| - ScopedProxy.new(self, block ? block.call(*args) : scope) + def method_missing(name, *args, &block) + name = name.to_s + if name =~ /\./ + # FIXME: Do we need this now? + call_method_chain(name, args, &block) + elsif create_automatic_scope(name) + send(name, *args, &block) + else + super(name.to_sym, *args, &block) end end + + def call_method_chain(chain, args, &block) + parts = chain.split(".") + s = parts[0..-2].inject(self) { |m, scope| m.send(scope) } + s.send(parts.last, *args) + end - module DefinedScopeProxyExtender + + def to_url_path + "#{name.underscore.pluralize}" + end + + def typed_id + HoboFields.to_name(self) || name.underscore.gsub("/", "__") + end + + end # --- of ClassMethods --- # + + + include Scopes + + + def to_url_path + "#{self.class.to_url_path}/#{to_param}" unless new_record? + end + + + def user_changes(user, changes={}) + if new_record? + self.attributes = changes + set_creator(user) + Hobo.can_create?(user, self) + else + original = duplicate + # 'duplicate' can cause these to be set, but they can conflict + # with the changes so we clear them + clear_aggregation_cache + clear_association_cache - attr_accessor :reflections + self.attributes = changes - def method_missing(name, *args, &block) - scope = (proxy_reflection.klass.respond_to?(:defined_scopes) and - scopes = proxy_reflection.klass.defined_scopes and - scopes[name.to_sym]) - - scope = scope.call(*args) if scope.is_a?(Proc) - - # If there's no :find, or :create specified, assume it's a find scope - find_scope = if scope && (scope.has_key?(:find) || scope.has_key?(:create)) - scope[:find] - else - scope - end - - if find_scope - scope_name = "@#{name.to_s.gsub('?','')}_scope" + Hobo.can_update?(user, original, self) + end + end + + + def user_changes!(user, changes={}) + user_changes(user, changes) or raise PermissionDeniedError + end + + + def user_can_create?(user, attributes={}) + raise ArgumentError, "Called #user_can_create? on existing record" unless new_record? + user_changes(user, attributes) + end + - # Calling instance_variable_get directly causes self to - # get loaded, hence this trick - assoc = Kernel.instance_method(:instance_variable_get).bind(self).call(scope_name) - - unless assoc - options = proxy_reflection.options - 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 - "(#{sanitize_sql scope_conditions}) AND (#{sanitize_sql has_many_conditions})" - else - scope_conditions || has_many_conditions - end - - 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 + def user_save_changes(user, changes={}) + user_changes!(user, changes) + save + end + - r = ActiveRecord::Reflection::AssociationReflection.new(:has_many, - name, - options, - proxy_owner.class) - - @reflections ||= {} - @reflections[name] = r - - assoc = if source - ActiveRecord::Associations::HasManyThroughAssociation - else - ActiveRecord::Associations::HasManyAssociation - end.new(self.proxy_owner, r) - - # Calling directly causes self to get loaded - Kernel.instance_method(:instance_variable_set).bind(self).call(scope_name, assoc) - end - assoc - else - super - end - end - - end - - - def has_many_with_defined_scopes(name, *args, &block) - options = args.extract_options! - if options.has_key?(:extend) || block - # Normal has_many - has_many_without_defined_scopes(name, *args + [options], &block) - else - options[:extend] = DefinedScopeProxyExtender - has_many_without_defined_scopes(name, *args + [options], &block) - end - end + def user_view(user, field=nil) + raise PermissionDeniedError unless Hobo.can_view?(user, self, field) end + def user_destroy(user) + raise PermissionDeniedError unless Hobo.can_delete?(user, self) + destroy + end + + def dependent_on self.class.dependent_on.map { |assoc| send(assoc) } end def attributes_with_hobo_type_conversion=(attributes, guard_protected_attributes=true) - converted = attributes.map_hash { |k, v| convert_type_for_mass_assignment(self.class.field_type(k), v) } + converted = attributes.map_hash { |k, v| convert_type_for_mass_assignment(self.class.attr_type(k), v) } send(:attributes_without_hobo_type_conversion=, converted, guard_protected_attributes) end def set_creator(user) + set_creator!(user) unless get_creator + end + + + def set_creator!(user) attr = self.class.creator_attribute return unless attr # Is creator a string field or an association? if self.class.reflections[attr] @@ -526,77 +367,86 @@ else # Assume it's a string field -- set it to the name of the current user self.send("#{attr}=", user.to_s) unless user.guest? end end + + + # We deliberately give this method an unconventional name to avoid + # polluting the application namespace too badly + def get_creator + self.class.creator_attribute && send(self.class.creator_attribute) + end def duplicate - res = self.class.new - res.instance_variable_set("@attributes", @attributes.dup) - res.instance_variable_set("@new_record", nil) unless new_record? + copy = self.class.new + copy.copy_instance_variables_from(self, ["@attributes_cache"]) + copy.instance_variable_set("@attributes", @attributes.dup) + copy.instance_variable_set("@new_record", nil) unless new_record? # Shallow copy of belongs_to associations for refl in self.class.reflections.values if refl.macro == :belongs_to and (target = self.send(refl.name)) - bta = ActiveRecord::Associations::BelongsToAssociation.new(res, refl) + bta = ActiveRecord::Associations::BelongsToAssociation.new(copy, refl) bta.replace(target) - res.instance_variable_set("@#{refl.name}", bta) + copy.instance_variable_set("@#{refl.name}", bta) end end - res + copy end def same_fields?(other, *fields) return true if other.nil? fields = fields.flatten fields.all?{|f| self.send(f) == other.send(f)} end + def only_changed_fields?(other, *changed_fields) return true if other.nil? - changed_fields = changed_fields.flatten.every(:to_s) - all_cols = self.class.columns.every(:name) - [] + changed_fields = changed_fields.flatten.*.to_s + all_cols = self.class.columns.*.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 - def created_date - created_at.to_date - end - - def modified_date - modified_at.to_date - end def typed_id - id ? "#{self.class.name.underscore}_#{self.id}" : nil + "#{self.class.name.underscore}_#{self.id}" if id end + def to_s if self.class.name_attribute send self.class.name_attribute else "#{self.class.name.titleize} #{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 field_type.is_a?(ActiveRecord::Reflection::AssociationReflection) && + field_type.macro.in?([:belongs_to, :has_one]) if value.is_a?(String) && value.starts_with?('@') + # TODO: This @foo_1 feature is rarely (never?) used - get rid of it Hobo.object_from_dom_id(value[1..-1]) else value end elsif !field_type.is_a?(Class) @@ -616,64 +466,21 @@ elsif value.is_a? String parse_datetime(value) else value end - elsif field_type <= TrueClass + elsif field_type <= Hobo::Boolean (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 - -module ActiveRecord::AttributeMethods::ClassMethods - - # Define an attribute reader method. Cope with nil column. - 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}'); ") - 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.not_in?(Hobo::Model::PLAIN_TYPES.values) - "val = begin; #{access_code}; 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