module Rfm # The Layout object represents a single FileMaker Pro layout. You use it to interact with # records in FileMaker. *All* access to FileMaker data is done through a layout, and this # layout determines which _table_ you actually hit (since every layout is explicitly associated # with a particular table -- see FileMakers Layout->Layout Setup dialog box). You never specify # _table_ information directly in RFM. # # Also, the layout determines which _fields_ will be returned. If a layout contains only three # fields from a large table, only those three fields are returned. If a layout includes related # fields from another table, they are returned as well. And if the layout includes portals, all # data in the portals is returned (see Record::portal for details). # # As such, you can _significantly_ improve performance by limiting what you put on the layout. # # =Using Layouts # # The Layout object is where you get most of your work done. It includes methods for all # FileMaker actions: # # * Layout::all # * Layout::any # * Layout::find # * Layout::edit # * Layout::create # * Layout::delete # # =Running Scripts # # In FileMaker, execution of a script must accompany another action. For example, to run a script # called _Remove Duplicates_ with a found set that includes everybody # named _Bill_, do this: # # myLayout.find({"First Name" => "Bill"}, :post_script => "Remove Duplicates") # # ==Controlling When the Script Runs # # When you perform an action in FileMaker, it always executes in this order: # # 1. Perform any find # 2. Sort the records # 3. Return the results # # You can control when in the process the script runs. Each of these options is available: # # * *post_script* tells FileMaker to run the script after finding and sorting # * *pre_find_script* tells FileMaker to run the script _before_ finding # * *pre_sort_script* tells FileMaker to run the script _before_ sorting, but _after_ finding # # ==Passing Parameters to a Script # # If you want to pass a parameter to the script, use the options above, but supply an array value # instead of a single string. For example: # # myLayout.find({"First Name" => "Bill"}, :post_script => ["Remove Duplicates", 10]) # # This sample runs the script called "Remove Duplicates" and passes it the value +10+ as its # script parameter. # # =Common Options # # Most of the methods on the Layout object accept an optional hash of +options+ to manipulate the # action. For example, when you perform a find, you will typiclaly get back _all_ matching records. # If you want to limit the number of records returned, you can do this: # # myLayout.find({"First Name" => "Bill"}, :max_records => 100) # # The +:max_records+ option tells FileMaker to limit the number of records returned. # # This is the complete list of available options: # # * *max_records* tells FileMaker how many records to return # # * *skip_records* tells FileMaker how many records in the found set to skip, before # returning results; this is typically combined with +max_records+ to "page" through # records # # * *sort_field* tells FileMaker to sort the records by the specified field # # * *sort_order* can be +descend+ or +ascend+ and determines the order # of the sort when +sort_field+ is specified # # * *post_script* tells FileMaker to perform a script after carrying out the action; you # can pass the script name, or a two-element array, with the script name first, then the # script parameter # # * *pre_find_script* is like +post_script+ except the script runs before any find is # performed # # * *pre_sort_script* is like +pre_find_script+ except the script runs after any find # and before any sort # # * *response_layout* tells FileMaker to switch layouts before producing the response; this # is useful when you need a field on a layout to perform a find, edit, or create, but you # want to improve performance by not including the field in the result # # * *logical_operator* can be +and+ or +or+ and tells FileMaker how to process multiple fields # in a find request # # * *modification_id* lets you pass in the modification id from a Record object with the request; # when you do, the action will fail if the record was modified in FileMaker after it was retrieved # by RFM but before the action was run # # # =Attributes # # The Layout object has a few useful attributes: # # * +name+ is the name of the layout # # * +field_controls+ is a hash of FieldControl objects, with the field names as keys. FieldControl's # tell you about the field on the layout: how is it formatted and what value list is assigned # # Note: It is possible to put the same field on a layout more than once. When this is the case, the # value in +field_controls+ for that field is an array with one element representing each instance # of the field. # # * +value_lists+ is a hash of arrays. The keys are value list names, and the values in the hash # are arrays containing the actual value list items. +value_lists+ will include every value # list that is attached to any field on the layout class Layout include ComplexQuery # Initialize a layout object. You never really need to do this. Instead, just do this: # # myServer = Rfm::Server.new(...) # myDatabase = myServer["Customers"] # myLayout = myDatabase["Details"] # # This sample code gets a layout object representing the Details layout in the Customers database # on the FileMaker server. # # In case it isn't obvious, this is more easily expressed this way: # # myServer = Rfm::Server.new(...) # myLayout = myServer["Customers"]["Details"] def initialize(name, db_obj) raise Rfm::Error::RfmError.new(0, "New instance of Rfm::Layout has no name.") if name.to_s == '' @name = name.to_s rfm_metaclass.instance_variable_set :@db, db_obj @loaded = false @field_controls = Rfm::CaseInsensitiveHash.new @value_lists = Rfm::CaseInsensitiveHash.new # @portal_meta = nil # @field_names = nil end meta_attr_reader :db attr_reader :name #, :db attr_writer :field_names, :portal_meta, :table def_delegator :db, :server alias_method :database, :db # These methods are to be inclulded in Layout and SubLayout, so that # they have their own descrete 'self' in the master class and the subclass. module LayoutModule # Returns a ResultSet object containing _every record_ in the table associated with this layout. def all(options = {}) get_records('-findall', {}, options) end # Returns a ResultSet containing a single random record from the table associated with this layout. def any(options = {}) get_records('-findany', {}, options) end # Finds a record. Typically you will pass in a hash of field names and values. For example: # # myLayout.find({"First Name" => "Bill"}) # # Values in the hash work just like value in FileMaker's Find mode. You can use any special # symbols (+==+, +...+, +>+, etc...). # # If you pass anything other than a hash as the first parameter, it is converted to a string and # assumed to be FileMaker's internal id for a record (the recid). def find(hash_or_recid, options = {}) if hash_or_recid.kind_of? Hash get_records('-find', hash_or_recid, options) else get_records('-find', {'-recid' => hash_or_recid.to_s}, options) end end # Updates the contents of the record whose internal +recid+ is specified. Send in a hash of new # data in the +values+ parameter. Returns a RecordSet containing the modified record. For example: # # recid = myLayout.find({"First Name" => "Bill"})[0].record_id # myLayout.edit(recid, {"First Name" => "Steve"}) # # The above code would find the first record with _Bill_ in the First Name field and change the # first name to _Steve_. def edit(recid, values, options = {}) get_records('-edit', {'-recid' => recid}.merge(values), options) end # Creates a new record in the table associated with this layout. Pass field data as a hash in the # +values+ parameter. Returns the newly created record in a RecordSet. You can use the returned # record to, ie, discover the values in auto-enter fields (like serial numbers). # # For example: # # result = myLayout.create({"First Name" => "Jerry", "Last Name" => "Robin"}) # id = result[0]["ID"] # # The above code adds a new record with first name _Jerry_ and last name _Robin_. It then # puts the value from the ID field (a serial number) into a ruby variable called +id+. def create(values, options = {}) get_records('-new', values, options) end # Deletes the record with the specified internal recid. Returns a ResultSet with the deleted record. # # For example: # # recid = myLayout.find({"First Name" => "Bill"})[0].record_id # myLayout.delete(recid) # # The above code finds every record with _Bill_ in the First Name field, then deletes the first one. def delete(recid, options = {}) get_records('-delete', {'-recid' => recid}, options) return nil end def get_records(action, extra_params = {}, options = {}) include_portals = options[:include_portals] ? options.delete(:include_portals) : nil xml_response = db.server.connect(db.account_name, db.password, action, params.merge(extra_params), options).body Rfm::Resultset.new(db.server, xml_response, self, include_portals) end def params {"-db" => db.name, "-lay" => self.name} end end # LayoutModule include LayoutModule def field_controls load unless @loaded @field_controls end def field_names load unless @field_names @field_names end def field_names_no_load @field_names end def value_lists load unless @loaded @value_lists end def total_count any.total_count end def portal_meta @portal_meta ||= any.portal_meta end def portal_meta_no_load @portal_meta end def portal_names portal_meta.keys end def table @table ||= any.table end def table_no_load @table end def load @loaded = true fmpxmllayout = db.server.load_layout(self) doc = XmlParser.new(fmpxmllayout.body, :namespace=>false, :parser=>server.state[:parser]) # check for errors error = doc['FMPXMLLAYOUT']['ERRORCODE'].to_s.to_i raise Rfm::Error::FileMakerError.getError(error) if error != 0 # process valuelists vlists = doc['FMPXMLLAYOUT']['VALUELISTS']['VALUELIST'] if !vlists.nil? #root.elements['VALUELISTS'].size > 0 vlists.each {|valuelist| name = valuelist['NAME'] @value_lists[name] = valuelist['VALUE'].collect{|value| Rfm::Metadata::ValueListItem.new(value['__content__'], value['DISPLAY'], name) } rescue [] } @value_lists.freeze end # process field controls doc['FMPXMLLAYOUT']['LAYOUT']['FIELD'].each {|field| name = field['NAME'] style = field['STYLE'] type = style['TYPE'] value_list_name = style['VALUELIST'] value_list = @value_lists[value_list_name] if value_list_name != '' field_control = Rfm::Metadata::FieldControl.new(name, type, value_list_name, value_list) existing = @field_controls[name] if existing if existing.kind_of?(Array) existing << field_control else @field_controls[name] = Array[existing, field_control] end else @field_controls[name] = field_control end } @field_names ||= @field_controls.collect{|k,v| v.name rescue v[0].name} @field_controls.freeze end private :load, :get_records, :params end # Layout end # Rfm