Skip to content
This repository
branch: 3-2-stable
Fetching contributors…

Cannot retrieve contributors at this time

file 221 lines (190 sloc) 8.492 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
require 'active_support/concern'

module ActiveRecord
  module AttributeAssignment
    extend ActiveSupport::Concern
    include ActiveModel::MassAssignmentSecurity

    module ClassMethods
      private

      # The primary key and inheritance column can never be set by mass-assignment for security reasons.
      def attributes_protected_by_default
        default = [ primary_key, inheritance_column ]
        default << 'id' unless primary_key.eql? 'id'
        default
      end
    end

    # Allows you to set all the attributes at once by passing in a hash with keys
    # matching the attribute names (which again matches the column names).
    #
    # If any attributes are protected by either +attr_protected+ or
    # +attr_accessible+ then only settable attributes will be assigned.
    #
    # class User < ActiveRecord::Base
    # attr_protected :is_admin
    # end
    #
    # user = User.new
    # user.attributes = { :username => 'Phusion', :is_admin => true }
    # user.username # => "Phusion"
    # user.is_admin? # => false
    def attributes=(new_attributes)
      return unless new_attributes.is_a?(Hash)

      assign_attributes(new_attributes)
    end

    # Allows you to set all the attributes for a particular mass-assignment
    # security role by passing in a hash of attributes with keys matching
    # the attribute names (which again matches the column names) and the role
    # name using the :as option.
    #
    # To bypass mass-assignment security you can use the :without_protection => true
    # option.
    #
    # class User < ActiveRecord::Base
    # attr_accessible :name
    # attr_accessible :name, :is_admin, :as => :admin
    # end
    #
    # user = User.new
    # user.assign_attributes({ :name => 'Josh', :is_admin => true })
    # user.name # => "Josh"
    # user.is_admin? # => false
    #
    # user = User.new
    # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
    # user.name # => "Josh"
    # user.is_admin? # => true
    #
    # user = User.new
    # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
    # user.name # => "Josh"
    # user.is_admin? # => true
    def assign_attributes(new_attributes, options = {})
      return if new_attributes.blank?

      attributes = new_attributes.stringify_keys
      multi_parameter_attributes = []
      nested_parameter_attributes = []
      @mass_assignment_options = options

      unless options[:without_protection]
        attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
      end

      attributes.each do |k, v|
        if k.include?("(")
          multi_parameter_attributes << [ k, v ]
        elsif respond_to?("#{k}=")
          if v.is_a?(Hash)
            nested_parameter_attributes << [ k, v ]
          else
            send("#{k}=", v)
          end
        else
          raise(UnknownAttributeError, "unknown attribute: #{k}")
        end
      end

      # assign any deferred nested attributes after the base attributes have been set
      nested_parameter_attributes.each do |k,v|
        send("#{k}=", v)
      end

      @mass_assignment_options = nil
      assign_multiparameter_attributes(multi_parameter_attributes)
    end

    protected

    def mass_assignment_options
      @mass_assignment_options ||= {}
    end

    def mass_assignment_role
      mass_assignment_options[:as] || :default
    end

    private

    # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
    # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
    # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
    # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
    # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
    # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
    # attribute will be set to nil.
    def assign_multiparameter_attributes(pairs)
      execute_callstack_for_multiparameter_attributes(
        extract_callstack_for_multiparameter_attributes(pairs)
      )
    end

    def instantiate_time_object(name, values)
      if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
        Time.zone.local(*values)
      else
        Time.time_with_datetime_fallback(self.class.default_timezone, *values)
      end
    end

    def execute_callstack_for_multiparameter_attributes(callstack)
      errors = []
      callstack.each do |name, values_with_empty_parameters|
        begin
          send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
        rescue => ex
          errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
        end
      end
      unless errors.empty?
        raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
      end
    end

    def read_value_from_parameter(name, values_hash_from_param)
      klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
      if values_hash_from_param.values.all?{|v|v.nil?}
        nil
      elsif klass == Time
        read_time_parameter_value(name, values_hash_from_param)
      elsif klass == Date
        read_date_parameter_value(name, values_hash_from_param)
      else
        read_other_parameter_value(klass, name, values_hash_from_param)
      end
    end

    def read_time_parameter_value(name, values_hash_from_param)
      # If Date bits were not provided, error
      raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
      max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
      # If Date bits were provided but blank, then return nil
      return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}

      set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
      # If Time bits are not there, then default to 0
      (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
      instantiate_time_object(name, set_values)
    end

    def read_date_parameter_value(name, values_hash_from_param)
      return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
      set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
      begin
        Date.new(*set_values)
      rescue ArgumentError # if Date.new raises an exception on an invalid date
        instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
      end
    end

    def read_other_parameter_value(klass, name, values_hash_from_param)
      max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
      values = (1..max_position).collect do |position|
        raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
        values_hash_from_param[position]
      end
      klass.new(*values)
    end

    def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
      [values_hash_from_param.keys.max,upper_cap].min
    end

    def extract_callstack_for_multiparameter_attributes(pairs)
      attributes = { }

      pairs.each do |pair|
        multiparameter_name, value = pair
        attribute_name = multiparameter_name.split("(").first
        attributes[attribute_name] = {} unless attributes.include?(attribute_name)

        parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
        attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
      end

      attributes
    end

    def type_cast_attribute_value(multiparameter_name, value)
      multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
    end

    def find_parameter_position(multiparameter_name)
      multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
    end

  end
end
Something went wrong with that request. Please try again.