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
# 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!
validate_find_options(options)
case args.first
when :first then find_initial(options)
when :all then find_every(options)
else find_from_ids(args, options)
end
end
# A convenience wrapper for find(:first, *args). You can pass in all the
# same arguments to this method as you can to find(:first).
def first(*args)
find(:first, *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.
# 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.
#
# ==== Examples
# # Create a single new object
# User.create(:first_name => 'Jamie')
#
# # Create an Array of new objects
# User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
#
# # Create a single object and pass it into a block to set other attributes.
# User.create(:first_name => 'Jamie') do |u|
# u.is_admin = false
# end
#
# # Creating an Array of new objects using a block, where the block is executed for each object:
# User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
# u.is_admin = false
# end
def create(attributes = nil, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, &block) }
else
object = new(attributes)
yield(object) if block_given?
object.save
object
end
end
private
def find_initial(options)
options.update(:limit => 1)
find_every(options)
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)
query = query_from_options(options)
SugarCRM.connection.get_entry_list(self._module.name, query, options)
end
def query_from_options(options)
# If we dont have conditions, just return an empty query
return "" unless options[:conditions]
conditions = []
options[:conditions].each_pair do |column, v|
v = [] << v unless v.class == Array
v.each{|value|
# parse operator in cases where (e.g.) :attribute => '>= some_value', fallback to '=' operator as default
operator = value.to_s[/^([<>=]*)(.*)$/,1]
operator = '=' if operator.nil? || operator.strip == ''
value = $2 # strip the operator from value passed to query
value = value.strip[/'?([^']*)'?/,1]
unless column =~ /_c$/ # attribute name ending with _c implies a custom attribute
condition_attribute = "#{self._module.table_name}.#{column}"
else
condition_attribute = column # if setting a condition on a custom attribute (i.e. created by user in Studio), don't add model table name (or query breaks)
end
conditions << "#{condition_attribute} #{operator} \'#{value}\'"
}
end
conditions.join(" AND ")
end
# Enables dynamic finders like find_by_user_name(user_name) and find_by_user_name_and_password(user_name, password)
# that are turned into find(:first, :conditions => ["user_name = ?", user_name]) and
# find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password]) respectively. Also works for
# find(:all) by using find_all_by_amount(50) that is turned into find(:all, :conditions => ["amount = ?", 50]).
#
# It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+
# is actually find_all_by_amount(amount, options).
#
# 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 do |r|
r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
end
#{'yield(record) if block_given?'}
#{'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_fields.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={})
@modified_attributes = {}
merge_attributes(attributes.with_indifferent_access)
clear_association_cache
@associations = self.class.associations_from_module_link_fields
define_attribute_methods
define_association_methods
typecast_attributes
self
end
def inspect
self
end
def to_s
attrs = []
@attributes.keys.sort.each do |k|
attrs << "#{k}: #{attribute_for_inspect(k)}"
end
"#<#{self.class} #{attrs.join(", ")}>"
end
# Saves the current object, checks that required fields are present.
# returns true or false
def save
return false if !changed?
return false if !valid?
begin
save!
rescue
return false
end
true
end
# Saves the current object, and any modified associations.
# Raises an exceptions if save fails for any reason.
def save!
save_modified_attributes
save_modified_associations
true
end
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)
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
# +select+ and leave the ID out, you're on your own, this predicate will return false.
#
# Note also that destroying a record preserves its ID in the model instance, so deleted
# models are still comparable.
def ==(comparison_object)
comparison_object.instance_of?(self.class) &&
id.present? &&
comparison_object.id == id
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 attribute_methods_generated?
self.class.attribute_methods_generated
end
def association_methods_generated?
self.class.association_methods_generated
end
Base.class_eval do
include AttributeMethods
extend AttributeMethods::ClassMethods
include AttributeValidations
include AttributeTypeCast
include AttributeSerializers
include AssociationMethods
extend AssociationMethods::ClassMethods
end
end; end