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)/ } # 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 find(*args, &block) options = args.extract_options! # add default sorting date (necessary for first and last methods to work) # most modules (Contacts, Accounts, etc.) use 'date_entered' to store when the record was created # other modules (e.g. EmailAddresses) use 'date_created' # Here, we account for this discrepancy... self.new # make sure the fields are loaded from SugarCRM so method_defined? will work properly if self.method_defined? :date_entered sort_criteria = 'date_entered' elsif self.method_defined? :date_created sort_criteria = 'date_created' # Added date_modified because TeamSets doesn't have a date_created or date_entered field. # There's no test for this because it's Pro and above only. # Hope this doesn't break anything! elsif self.method_defined? :date_modified sort_criteria = 'date_modified' else raise InvalidAttribute, "Unable to determine record creation date for sorting criteria: expected date_entered, date_created, or date_modified attribute to be present" end options = {:order_by => sort_criteria}.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].to_s) rescue Exception => e raise end find_initial(options) when :all Array.wrap(find_every(options, &block)).compact else find_from_ids(args, options, &block) end end # return the connection to the correct SugarCRM server (there can be several) def connection self.session.connection end # return the number of records satifsying the options # note: the REST API has a bug (documented with Sugar as bug 43339) where passing custom attributes in the options will result in the # options being ignored and '0' being returned, regardless of the existence of records satisfying the options def count(options={}) raise InvalidAttribute, 'Conditions on custom attributes are not supported due to REST API bug' if contains_custom_attribute(options[:conditions]) query = query_from_options(options) connection.get_entries_count(self._module.name, query, options)['result_count'].to_i 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, &block) find(:first, *args, &block) end # A convenience wrapper for find(:last, *args). You can pass in all the # same arguments to this method as you can to find(:last). def last(*args, &block) find(:last, *args, &block) 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, &block) find(:all, *args, &block) 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 end # Creates an instance of a Module Class, i.e. Account, User, Contact, etc. def initialize(attributes={}, &block) attributes.delete('id') @errors = {} @modified_attributes = {} merge_attributes(attributes.with_indifferent_access) clear_association_cache 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 # objects are considered equal if they represent the same SugarCRM record # this behavior is required for Rails to be able to properly cast objects to json (lists, in particular) def equal?(other) return false unless other && other.respond_to?(:id) self.id == other.id end # return variables that are defined in SugarCRM, instead of the object's actual variables (such as modified_attributes, errors, etc.) def instance_variables @_instance_variables ||= @attributes.keys.map{|i| ('@' + i).to_sym } end # override to return the value of the SugarCRM record's attributes def instance_variable_get(name) name = name.to_s.gsub(/^@/,'') @attributes[name] end # Rails requires this to (e.g.) generate json representations of models # this code taken directly from the Rails project if defined?(Rails) def instance_values Hash[instance_variables.map { |name| [name.to_s[1..-1], instance_variable_get(name)] }] end end def to_json(options={}) attributes.to_json end def to_xml(options={}) attributes.to_xml end # Saves the current object, checks that required fields are present. # returns true or false def save(opts={}) options = { :validate => true }.merge(opts) return false if !(new_record? || changed?) if options[:validate] return false if !valid? end begin save!(options) 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!(opts={}) save_modified_attributes!(opts) save_modified_associations! true end def delete return false if id.blank? params = {} params[:id] = serialize_id params[:deleted]= {:name => "deleted", :value => "1"} @attributes[:deleted] = (self.class.connection.set_entry(self.class._module.name, params).class == Hash) end alias :destroy :delete # Returns if the record is persisted, i.e. it’s not a new record and it was not destroyed def persisted? !(new_record? || destroyed?) end # Reloads the record from SugarCRM def reload! self.attributes = self.class.find(self.id).attributes end def blank? @attributes.empty? end alias :empty? :blank? # 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 alias :eql? :== def update_attribute!(name, value) self.send("#{name}=".to_sym, value) self.save! end def update_attribute(name, value) begin update_attribute!(name, value) rescue return false end true end def update_attributes!(attributes) attributes.each do |name, value| self.send("#{name}=".to_sym, value) end self.save! end def update_attributes(attributes) begin update_attributes!(attributes) rescue return false end true end # Returns the URL (in string format) where the module instance is available in CRM def url "#{SugarCRM.session.config[:base_url]}/index.php?module=#{self.class._module.name}&action=DetailView&record=#{self.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 pretty_print(pp) pp.text self.inspect.to_s, 0 end def attribute_methods_generated? self.class.attribute_methods_generated end def association_methods_generated? self.class.association_methods_generated end def to_key new_record? ? nil : [ id ] end def to_param id.to_s end def is_a?(klass) superclasses.include? klass end alias :kind_of? :is_a? alias :=== :is_a? private # returns true if the hash contains a custom attribute created in Studio (and whose name therefore ends in '_c') def self.contains_custom_attribute(attributes) attributes ||= {} attributes.each_key{|k| return true if k.to_s =~ /_c$/ } false end def superclasses return @superclasses if @superclasses @superclasses = [self.class] current_class = self.class while current_class.respond_to? :superclass @superclasses << (current_class = current_class.superclass) end @superclasses end Base.class_eval do extend FinderMethods::ClassMethods include AttributeMethods extend AttributeMethods::ClassMethods include AttributeValidations include AttributeTypeCast include AttributeSerializers include AssociationMethods extend AssociationMethods::ClassMethods include AssociationCache end end; end