lib/sugarcrm/base.rb in sugarcrm-0.9.10 vs lib/sugarcrm/base.rb in sugarcrm-0.9.11
- old
+ new
@@ -1,67 +1,78 @@
module SugarCRM; class Base
# Unset all of the instance methods we don't need.
instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$|^define_method$|^class$|^nil.$|^methods$|^instance_of.$|^respond_to)/ }
- # This holds our connection
- cattr_accessor :connection, :instance_writer => false
-
# Tracks if we have extended our class with attribute methods yet.
class_attribute :attribute_methods_generated
self.attribute_methods_generated = false
class_attribute :association_methods_generated
self.association_methods_generated = false
class_attribute :_module
self._module = nil
+ # the session to which we're linked
+ class_attribute :session
+ self.session = nil
+
# Contains a list of attributes
attr :attributes, true
attr :modified_attributes, true
attr :associations, true
attr :debug, true
attr :errors, true
class << self # Class methods
- def establish_connection(url, user, pass, opts={})
- options = {
- :debug => false,
- :register_modules => true
- }.merge(opts)
- @debug = options[:debug]
- @@connection = SugarCRM::Connection.new(url, user, pass, options)
- end
-
def find(*args)
options = args.extract_options!
+ options = {:order_by => 'date_entered'}.merge(options)
validate_find_options(options)
case args.first
when :first
find_initial(options)
+ when :last
+ begin
+ options[:order_by] = reverse_order_clause(options[:order_by])
+ rescue Exception => e
+ raise
+ end
+ find_initial(options)
when :all
Array.wrap(find_every(options)).compact
else
find_from_ids(args, options)
end
end
+
+ # return the connection to the correct SugarCRM server (there can be several)
+ def connection
+ self.parent.session.connection
+ end
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
# same arguments to this method as you can to <tt>find(:first)</tt>.
def first(*args)
find(:first, *args)
end
+ # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the
+ # same arguments to this method as you can to <tt>find(:last)</tt>.
+ def last(*args)
+ find(:last, *args)
+ end
+
# This is an alias for find(:all). You can pass in all the same arguments to this method as you can
# to find(:all)
def all(*args)
find(:all, *args)
end
- # Creates an object (or multiple objects) and saves it to SugarCRM, if validations pass.
+ # Creates an object (or multiple objects) and saves it to SugarCRM if validations pass.
# The resulting object is returned whether the object was saved successfully to the database or not.
#
# The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the
# attributes on the objects that are to be created.
#
@@ -90,230 +101,10 @@
object.save
object
end
end
- private
-
- def find_initial(options)
- options.update(:limit => 1)
- result = find_by_sql(options)
- return result.first if result.instance_of? Array # find_by_sql will return an Array if result are found
- result
- end
-
- def find_from_ids(ids, options)
- expects_array = ids.first.kind_of?(Array)
- return ids.first if expects_array && ids.first.empty?
-
- ids = ids.flatten.compact.uniq
-
- case ids.size
- when 0
- raise RecordNotFound, "Couldn't find #{self._module.name} without an ID"
- when 1
- result = find_one(ids.first, options)
- expects_array ? [ result ] : result
- else
- find_some(ids, options)
- end
- end
-
- def find_one(id, options)
- if result = SugarCRM.connection.get_entry(self._module.name, id, {:fields => self._module.fields.keys})
- result
- else
- raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
- end
- end
-
- def find_some(ids, options)
- result = SugarCRM.connection.get_entries(self._module.name, ids, {:fields => self._module.fields.keys})
-
- # Determine expected size from limit and offset, not just ids.size.
- expected_size =
- if options[:limit] && ids.size > options[:limit]
- options[:limit]
- else
- ids.size
- end
-
- # 11 ids with limit 3, offset 9 should give 2 results.
- if options[:offset] && (ids.size - options[:offset] < expected_size)
- expected_size = ids.size - options[:offset]
- end
-
- if result.size == expected_size
- result
- else
- raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
- end
- end
-
- def find_every(options)
- find_by_sql(options)
- end
-
- def find_by_sql(options)
- # SugarCRM REST API has a bug where, when :limit and :offset options are passed simultaneously, :limit is considered to be the smallest of the two, and :offset is the larger
- # in addition to allowing querying of large datasets while avoiding timeouts,
- # this implementation fixes the :limit - :offset bug so that it behaves correctly
- local_options = {}
- options.keys.each{|k|
- local_options[k] = options[k]
- }
- local_options.delete(:offset) if local_options[:offset] == 0
-
- # store the number of records wanted by user, as we'll overwrite :limit option to obtain several slices of records (to avoid timeout issues)
- nb_to_fetch = local_options[:limit]
- nb_to_fetch = nb_to_fetch.to_i if nb_to_fetch
- offset_value = local_options[:offset] || 10 # arbitrary value, must be bigger than :limit used (see comment above)
- offset_value = offset_value.to_i
- offset_value.freeze
- initial_limit = nb_to_fetch.nil? ? offset_value : [offset_value, nb_to_fetch].min # how many records should be fetched on first pass
- # ensure results are ordered so :limit and :offset option behave in a deterministic fashion
- local_options = { :order_by => :id }.merge(local_options)
- local_options.update(:limit => initial_limit) # override original argument
-
- # get first slice of results
- # note: to work around a SugarCRM REST API bug, the :limit option must always be smaller than the :offset option
- # this is the reason this first query is separate (not in the loop): the initial query has a larger limit, so that we can then use the loop
- # with :limit always smaller than :offset
- results = SugarCRM.connection.get_entry_list(self._module.name, query_from_options(local_options), local_options)
- return nil unless results
- results = Array.wrap(results)
-
- limit_value = [5, offset_value].min # arbitrary value, must be smaller than :offset used (see comment above)
- limit_value.freeze
- local_options = { :order_by => :id }.merge(local_options)
- local_options.update(:limit => limit_value)
-
- # a portion of the results has already been queried
- # update or set the :offset value to reflect this
- local_options[:offset] ||= results.size
- local_options[:offset] += offset_value
-
- # continue fetching results until we either
- # a) have as many results as the user wants (specified via the original :limit option)
- # b) there are no more results matching the criteria
- while result_slice = SugarCRM.connection.get_entry_list(self._module.name, query_from_options(local_options), local_options)
- results.concat(Array.wrap(result_slice))
- # make sure we don't return more results than the user requested (via original :limit option)
- if nb_to_fetch && results.size >= nb_to_fetch
- return results.slice(0, nb_to_fetch)
- end
- local_options[:offset] += local_options[:limit] # update :offset as we get more records
- end
- results
- end
-
- def query_from_options(options)
- # If we dont have conditions, just return an empty query
- return "" unless options[:conditions]
- conditions = []
- options[:conditions].each do |condition|
- # Merge the result into the conditions array
- conditions |= flatten_conditions_for(condition)
- end
- conditions.join(" AND ")
- end
-
- # Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt>
- # that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and
- # <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for
- # <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>.
- #
- # It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+
- # is actually <tt>find_all_by_amount(amount, options)</tt>.
- #
- # Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
- # are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
- # respectively.
- #
- # Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future
- # attempts to use it do not run through method_missing.
- def method_missing(method_id, *arguments, &block)
- if match = DynamicFinderMatch.match(method_id)
- attribute_names = match.attribute_names
- super unless all_attributes_exists?(attribute_names)
- if match.finder?
- finder = match.finder
- bang = match.bang?
- self.class_eval <<-EOS, __FILE__, __LINE__ + 1
- def self.#{method_id}(*args)
- options = args.extract_options!
- attributes = construct_attributes_from_arguments(
- [:#{attribute_names.join(',:')}],
- args
- )
- finder_options = { :conditions => attributes }
- validate_find_options(options)
-
- #{'result = ' if bang}if options[:conditions]
- with_scope(:find => finder_options) do
- find(:#{finder}, options)
- end
- else
- find(:#{finder}, options.merge(finder_options))
- end
- #{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
- end
- EOS
- send(method_id, *arguments)
- elsif match.instantiator?
- instantiator = match.instantiator
- self.class_eval <<-EOS, __FILE__, __LINE__ + 1
- def self.#{method_id}(*args)
- attributes = [:#{attribute_names.join(',:')}]
- protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
- args.each_with_index do |arg, i|
- if arg.is_a?(Hash)
- protected_attributes_for_create = args[i].with_indifferent_access
- else
- unprotected_attributes_for_create[attributes[i]] = args[i]
- end
- end
-
- find_attributes = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes)
-
- options = { :conditions => find_attributes }
-
- record = find(:first, options)
-
- if record.nil?
- record = self.new(unprotected_attributes_for_create)
- #{'record.save' if instantiator == :create}
- record
- else
- record
- end
- end
- EOS
- send(method_id, *arguments, &block)
- end
- else
- super
- end
- end
-
- def all_attributes_exists?(attribute_names)
- attribute_names.all? { |name| attributes_from_module.include?(name) }
- end
-
- def construct_attributes_from_arguments(attribute_names, arguments)
- attributes = {}
- attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
- attributes
- end
-
- VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
- :order_by, :select, :readonly, :group, :having, :from, :lock ]
-
- def validate_find_options(options) #:nodoc:
- options.assert_valid_keys(VALID_FIND_OPTIONS)
- end
-
end
# Creates an instance of a Module Class, i.e. Account, User, Contact, etc.
def initialize(attributes={}, &block)
@modified_attributes = {}
@@ -361,13 +152,18 @@
def delete
return false if id.blank?
params = {}
params[:id] = serialize_id
params[:deleted]= {:name => "deleted", :value => "1"}
- (SugarCRM.connection.set_entry(self.class._module.name, params).class == Hash)
+ (self.class.connection.set_entry(self.class._module.name, params).class == Hash)
end
+ # Reloads the record from SugarCRM
+ def reload!
+ self.attributes = self.class.find(self.id).attributes
+ end
+
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
# is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
#
# Note that new records are different from any other record by definition, unless the
# other record is the receiver itself. Besides, if you fetch existing records with
@@ -391,25 +187,30 @@
attributes.each do |name, value|
self.send("#{name}=".to_sym, value)
end
self.save
end
-
+
# Delegates to id in order to allow two records of the same type and id to work with something like:
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
id.hash
end
-
+
+ def pretty_print(pp)
+ pp.text self.inspect, 0
+ end
+
def attribute_methods_generated?
self.class.attribute_methods_generated
end
def association_methods_generated?
self.class.association_methods_generated
end
Base.class_eval do
+ extend FinderMethods::ClassMethods
include AttributeMethods
extend AttributeMethods::ClassMethods
include AttributeValidations
include AttributeTypeCast
include AttributeSerializers
\ No newline at end of file