Skip to content
This repository
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 178 lines (151 sloc) 6.307 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
module ActiveRecord
  class CompositeKeyError < StandardError #:nodoc:
  end

  class Base
    INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys'
    NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet'

    class << self
      def set_primary_keys(*keys)
        keys = keys.first if keys.first.is_a?(Array)

        if keys.length == 1
          set_primary_key(keys.first)
          return
        end

        cattr_accessor :primary_keys
        self.primary_keys = keys.map { |k| k.to_sym }

        class_eval <<-EOV
extend CompositeClassMethods
include CompositeInstanceMethods
include CompositePrimaryKeys::ActiveRecord::AssociationPreload
EOV
      end

      def composite?
        false
      end
    end

    def composite?
      self.class.composite?
    end

    def [](attr_name)
      # CPK
      if attr_name.is_a?(String) and attr_name != attr_name.split(CompositePrimaryKeys::ID_SEP).first
        attr_name = attr_name.split(CompositePrimaryKeys::ID_SEP)
      end

      # CPK
      if attr_name.is_a?(Array)
        values = attr_name.map {|name| read_attribute(name)}
        CompositePrimaryKeys::CompositeKeys.new(values)
      else
        read_attribute(attr_name)
      end
    end

    def []=(attr_name, value)
      # CPK
      if attr_name.is_a?(String) and attr_name != attr_name.split(CompositePrimaryKeys::ID_SEP).first
        attr_name = attr_name.split(CompositePrimaryKeys::ID_SEP)
      end

      if attr_name.is_a? Array
        unless value.length == attr_name.length
          raise "Number of attr_names and values do not match"
        end
        [attr_name, value].transpose.map {|name,val| write_attribute(name, val)}
        value
      else
         write_attribute(attr_name, value)
      end
    end
    
    module CompositeClassMethods
      def primary_key
        primary_keys
      end

      def primary_key=(keys)
        primary_keys = keys
      end

      def composite?
        true
      end

      #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)"
      #ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3"
      def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')
        many_ids.map {|ids| "#{left_bracket}#{CompositePrimaryKeys::CompositeKeys.new(ids)}#{right_bracket}"}.join(list_sep)
      end

      def relation #:nodoc:
        @relation ||= begin
          result = Relation.new(self, arel_table)
          # CPK
          class << result
            include CompositePrimaryKeys::ActiveRecord::FinderMethods::InstanceMethods
            include CompositePrimaryKeys::ActiveRecord::Relation::InstanceMethods
          end
          result
        end

        finder_needs_type_condition? ? @relation.where(type_condition) : @relation
      end
    end

    module CompositeInstanceMethods
      # A model instance's primary keys is always available as model.ids
      # whether you name it the default 'id' or set it to something else.
      def id
        attr_names = self.class.primary_keys
        ::CompositePrimaryKeys::CompositeKeys.new(attr_names.map { |attr_name| read_attribute(attr_name) })
      end
      alias_method :ids, :id

      def ids_hash
        self.class.primary_key.zip(ids).inject(Hash.new) do |hash, (key, value)|
          hash[key] = value
          hash
        end
      end

      def id_before_type_cast
        self.class.primary_keys.map do |key|
          self.send("#{key.to_s}_before_type_cast")
        end
      end

      def quoted_id #:nodoc:
        [self.class.primary_keys, ids].
          transpose.
          map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}
      end

      # Sets the primary ID.
      def id=(ids)
        ids = ids.split(CompositePrimaryKeys::ID_SEP) if ids.is_a?(String)
        ids.flatten!
        unless ids.is_a?(Array) and ids.length == self.class.primary_keys.length
          raise "#{self.class}.id= requires #{self.class.primary_keys.length} ids"
        end
        [primary_keys, ids].transpose.each {|key, an_id| write_attribute(key , an_id)}
        id
      end

      def ==(comparison_object)
        ids.is_a?(Array) ? super(comparison_object) && ids.all? {|id| id.present?} : super(comparison_object)
      end

      # Cloned objects have no id assigned and are treated as new records. Note that this is a "shallow" clone
      # as it copies the object's attributes only, not its associations. The extent of a "deep" clone is
      # application specific and is therefore left to the application to implement according to its need.
      def initialize_copy(other)
        # Think the assertion which fails if the after_initialize callback goes at the end of the method is wrong. The
        # deleted clone method called new which therefore called the after_initialize callback. It then went on to copy
        # over the attributes. But if it's copying the attributes afterwards then it hasn't finished initializing right?
        # For example in the test suite the topic model's after_initialize method sets the author_email_address to
        # test@test.com. I would have thought this would mean that all cloned models would have an author email address
        # of test@test.com. However the test_clone test method seems to test that this is not the case. As a result the
        # after_initialize callback has to be run *before* the copying of the atrributes rather than afterwards in order
        # for all tests to pass. This makes no sense to me.
        callback(:after_initialize) if respond_to_without_attributes?(:after_initialize)
        cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
        # CPK
        #cloned_attributes.delete(self.class.primary_key)
        self.class.primary_key.each {|key| cloned_attributes.delete(key.to_s)}

        @attributes = cloned_attributes
        clear_aggregation_cache
        @attributes_cache = {}
        @new_record = true
        ensure_proper_type

        if scope = self.class.send(:current_scoped_methods)
          create_with = scope.scope_for_create
          create_with.each { |att,value| self.send("#{att}=", value) } if create_with
        end
      end
    end
  end
end
Something went wrong with that request. Please try again.