ginjo-rfm 2.0.pre31 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rfm.rb CHANGED
@@ -5,10 +5,9 @@ end
5
5
 
6
6
  require 'thread' # some versions of ActiveSupport will raise error about Mutex unless 'thread' is loaded.
7
7
  require 'active_support'
8
- # ActiveSupport appears to load these automatcially
9
- #require 'active_support/core_ext/object/blank'
10
- #require 'active_support/ordered_hash'
11
- require 'active_support/version'
8
+ require 'active_support/core_ext/object/blank'
9
+ require 'active_support/ordered_hash'
10
+ require 'active_support/version'
12
11
  require 'rfm/utilities/core_ext'
13
12
  require 'rfm/utilities/case_insensitive_hash'
14
13
  #require 'rfm/utilities/config'
@@ -29,9 +28,9 @@ module Rfm
29
28
  autoload :Record, 'rfm/record'
30
29
  autoload :Base, 'rfm/base'
31
30
  autoload :XmlParser, 'rfm/utilities/xml_parser'
32
- autoload :ComplexQuery, 'rfm/utilities/complex_query'
33
31
  autoload :Config, 'rfm/utilities/config'
34
32
  autoload :Factory, 'rfm/utilities/factory'
33
+ autoload :CompoundQuery,'rfm/utilities/compound_query'
35
34
  autoload :VERSION, 'rfm/version'
36
35
 
37
36
  module Metadata
data/lib/rfm/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.pre31
1
+ 2.0.0
data/lib/rfm/base.rb CHANGED
@@ -31,12 +31,6 @@ module Rfm
31
31
  # @person.save
32
32
  #
33
33
  #
34
- #
35
- #
36
- # :nodoc: TODO: make sure all methods return something (a record?) if successful, otherwise nil or error
37
- # :nodoc: TODO: move rfm methods & patches from fetch_mail_with_ruby to this file
38
- #
39
- # TODO: Are these really needed here?
40
34
  require 'rfm/database'
41
35
  require 'rfm/layout'
42
36
  require 'rfm/record'
@@ -169,7 +163,7 @@ module Rfm
169
163
  include ActiveModel::Validations
170
164
  include ActiveModel::Serialization
171
165
  extend ActiveModel::Callbacks
172
- define_model_callbacks(:create, :update, :destroy)
166
+ define_model_callbacks(:create, :update, :destroy, :validate)
173
167
  rescue LoadError, StandardError
174
168
  def run_callbacks(*args)
175
169
  yield
@@ -199,7 +193,7 @@ module Rfm
199
193
 
200
194
  # Access layout functions from base model
201
195
  def_delegators :layout, :db, :server, :field_controls, :field_names, :value_lists, :total_count,
202
- :query, :all, :delete, :portal_meta, :portal_names, :database, :table
196
+ :query, :all, :delete, :portal_meta, :portal_names, :database, :table, :count
203
197
 
204
198
  def inherited(model)
205
199
  (Rfm::Factory.models << model).uniq unless Rfm::Factory.models.include? model
@@ -230,9 +224,9 @@ module Rfm
230
224
  end
231
225
 
232
226
  # Just like Layout#find, but searching by record_id will return a record, not a resultset.
233
- def find(*args)
234
- r = layout.find(*args)
235
- if args[0].class != Hash and r.size == 1
227
+ def find(find_criteria, options={})
228
+ r = layout.find(find_criteria, options={})
229
+ if ![Hash,Array].include?(find_criteria.class) and r.size == 1
236
230
  r[0]
237
231
  else
238
232
  r
data/lib/rfm/database.rb CHANGED
@@ -66,7 +66,7 @@ module Rfm
66
66
  #
67
67
  # This sample code gets a database object representing the Customers database on the FileMaker server.
68
68
  def initialize(name, server_obj, acnt=nil, pass=nil)
69
- raise Rfm::Error::RfmError.new(0, "New instance of Rfm::Database has no name.") if name.to_s == ''
69
+ raise Rfm::Error::RfmError.new(0, "New instance of Rfm::Database has no name. Attempted name '#{name}'.") if name.to_s == ''
70
70
  @name = name.to_s
71
71
  rfm_metaclass.instance_variable_set :@server, server_obj
72
72
  @account_name = acnt #server.state[:account_name] or ""
data/lib/rfm/layout.rb CHANGED
@@ -119,9 +119,7 @@ module Rfm
119
119
  # list that is attached to any field on the layout
120
120
 
121
121
  class Layout
122
-
123
- include ComplexQuery
124
-
122
+
125
123
  # Initialize a layout object. You never really need to do this. Instead, just do this:
126
124
  #
127
125
  # myServer = Rfm::Server.new(...)
@@ -136,7 +134,7 @@ module Rfm
136
134
  # myServer = Rfm::Server.new(...)
137
135
  # myLayout = myServer["Customers"]["Details"]
138
136
  def initialize(name, db_obj)
139
- raise Rfm::Error::RfmError.new(0, "New instance of Rfm::Layout has no name.") if name.to_s == ''
137
+ raise Rfm::Error::RfmError.new(0, "New instance of Rfm::Layout has no name. Attempted name '#{name}'.") if name.to_s == ''
140
138
  @name = name.to_s
141
139
  rfm_metaclass.instance_variable_set :@db, db_obj
142
140
 
@@ -156,6 +154,10 @@ module Rfm
156
154
 
157
155
  # These methods are to be inclulded in Layout and SubLayout, so that
158
156
  # they have their own descrete 'self' in the master class and the subclass.
157
+ # This means these methods will not get forwarded, and will talk to the correct
158
+ # variables & objects of the correct self.
159
+ # Do not get or set instance variables in Layout from other objects directly,
160
+ # always use getter & setter methods.
159
161
  module LayoutModule
160
162
 
161
163
  # Returns a ResultSet object containing _every record_ in the table associated with this layout.
@@ -175,14 +177,30 @@ module Rfm
175
177
  # Values in the hash work just like value in FileMaker's Find mode. You can use any special
176
178
  # symbols (+==+, +...+, +>+, etc...).
177
179
  #
178
- # If you pass anything other than a hash as the first parameter, it is converted to a string and
180
+ # Create a Filemaker 'omit' request by including an :omit key with a value of true.
181
+ #
182
+ # myLayout.find :field1 => 'val1', :field2 => 'val2', :omit => true
183
+ #
184
+ # Create multiple Filemaker find requests by passing an array of hashes to the #find method.
185
+ #
186
+ # myLayout.find [{:field1 => 'bill', :field2 => 'admin'}, {:field3 => 'inactive', :omit => true}, ...]
187
+ #
188
+ # If the value of a field in a find request is an array of strings, the string values will be logically OR'd in the query.
189
+ #
190
+ # myLayout.find :fieldOne => ['bill','mike','bob'], :fieldTwo =>'staff'
191
+ #
192
+ # If you pass anything other than a hash or an array as the first parameter, it is converted to a string and
179
193
  # assumed to be FileMaker's internal id for a record (the recid).
180
- def find(hash_or_recid, options = {})
181
- if hash_or_recid.kind_of? Hash
182
- get_records('-find', hash_or_recid, options)
183
- else
184
- get_records('-find', {'-recid' => hash_or_recid.to_s}, options)
185
- end
194
+ #
195
+ # myLayout.find 54321
196
+ #
197
+ def find(find_criteria, options = {})
198
+ get_records(*Rfm::CompoundQuery.new(find_criteria, options))
199
+ end
200
+
201
+ # Access to raw -findquery command.
202
+ def query(query_hash, options = {})
203
+ get_records('-findquery', query_hash, options)
186
204
  end
187
205
 
188
206
  # Updates the contents of the record whose internal +recid+ is specified. Send in a hash of new
@@ -195,6 +213,7 @@ module Rfm
195
213
  # first name to _Steve_.
196
214
  def edit(recid, values, options = {})
197
215
  get_records('-edit', {'-recid' => recid}.merge(values), options)
216
+ #get_records('-edit', {'-recid' => recid}.merge(expand_repeats(values)), options) # attempt to set repeating fields.
198
217
  end
199
218
 
200
219
  # Creates a new record in the table associated with this layout. Pass field data as a hash in the
@@ -225,6 +244,11 @@ module Rfm
225
244
  return nil
226
245
  end
227
246
 
247
+ # Retrieves metadata only, with an empty resultset.
248
+ def view(options = {})
249
+ get_records('-view', {}, options)
250
+ end
251
+
228
252
  def get_records(action, extra_params = {}, options = {})
229
253
  include_portals = options[:include_portals] ? options.delete(:include_portals) : nil
230
254
  xml_response = db.server.connect(db.account_name, db.password, action, params.merge(extra_params), options).body
@@ -234,6 +258,28 @@ module Rfm
234
258
  def params
235
259
  {"-db" => db.name, "-lay" => self.name}
236
260
  end
261
+
262
+
263
+ # Intended to set each repeat individually but doesn't work with FM
264
+ def expand_repeats(hash)
265
+ hash.each do |key,val|
266
+ if val.kind_of? Array
267
+ val.each_with_index{|v, i| hash["#{key}(#{i+1})"] = v}
268
+ hash.delete(key)
269
+ end
270
+ end
271
+ hash
272
+ end
273
+
274
+ # Intended to brute-force repeat setting but doesn't work with FM
275
+ def join_repeats(hash)
276
+ hash.each do |key,val|
277
+ if val.kind_of? Array
278
+ hash[key] = val.join('\x1D')
279
+ end
280
+ end
281
+ hash
282
+ end
237
283
 
238
284
  end # LayoutModule
239
285
  include LayoutModule
@@ -258,12 +304,16 @@ module Rfm
258
304
  @value_lists
259
305
  end
260
306
 
307
+ def count(find_criteria, options={})
308
+ find(find_criteria, options.merge({:max_records => 0})).foundset_count
309
+ end
310
+
261
311
  def total_count
262
- any.total_count
312
+ view.total_count
263
313
  end
264
314
 
265
315
  def portal_meta
266
- @portal_meta ||= any.portal_meta
316
+ @portal_meta ||= view.portal_meta
267
317
  end
268
318
 
269
319
  def portal_meta_no_load
@@ -275,7 +325,7 @@ module Rfm
275
325
  end
276
326
 
277
327
  def table
278
- @table ||= any.table
328
+ @table ||= view.table
279
329
  end
280
330
 
281
331
  def table_no_load
data/lib/rfm/record.rb CHANGED
@@ -57,6 +57,8 @@ module Rfm
57
57
  # In the above example, the Price field is a repeating field. The code puts the first repetition in a variable called
58
58
  # +val1+ and the second in a variable called +val2+.
59
59
  #
60
+ # It is not currently possible to create or edit a record's repeating fields beyond the first repitition, using Rfm.
61
+ #
60
62
  # =Accessing Portals
61
63
  #
62
64
  # If the ResultSet includes portals (because the layout it comes from has portals on it) you can access them
@@ -108,7 +110,7 @@ module Rfm
108
110
 
109
111
  attr_accessor :layout, :resultset
110
112
  attr_reader :record_id, :mod_id, :portals
111
- def_delegators :resultset, :field_meta
113
+ def_delegators :resultset, :field_meta, :portal_names
112
114
  def_delegators :layout, :db, :database, :server
113
115
 
114
116
  def initialize(record, resultset_obj, field_meta, layout_obj, portal=nil)
data/lib/rfm/server.rb CHANGED
@@ -193,7 +193,7 @@ module Rfm
193
193
  # :root_cert_path => '/usr/cert_file/'
194
194
  # })
195
195
  def initialize(options)
196
- raise Rfm::Error::RfmError.new(0, "New instance of Rfm::Server has no host name.") if options[:host].to_s == ''
196
+ raise Rfm::Error::RfmError.new(0, "New instance of Rfm::Server has no host name. Attempted name '#{options[:host]}'.") if options[:host].to_s == ''
197
197
 
198
198
  @state = {
199
199
  :host => 'localhost',
@@ -290,8 +290,10 @@ module Rfm
290
290
  raise Rfm::CommunicationError.new("While trying to reach the Web Publishing Engine, RFM was redirected too many times.") if limit == 0
291
291
 
292
292
  if @state[:log_actions] == true
293
- qs = post_data.collect{|key,val| "#{CGI::escape(key.to_s)}=#{CGI::escape(val.to_s)}"}.join("&")
294
- warn "#{@scheme}://#{@host_name}:#{@port}#{path}?#{qs}"
293
+ #qs = post_data.collect{|key,val| "#{CGI::escape(key.to_s)}=#{CGI::escape(val.to_s)}"}.join("&")
294
+ qs_unescaped = post_data.collect{|key,val| "#{key.to_s}=#{val.to_s}"}.join("&")
295
+ #warn "#{@scheme}://#{@host_name}:#{@port}#{path}?#{qs}"
296
+ warn "#{@scheme}://#{@host_name}:#{@port}#{path}?#{qs_unescaped}"
295
297
  end
296
298
 
297
299
  request = Net::HTTP::Post.new(path)
@@ -0,0 +1,113 @@
1
+ module Rfm
2
+
3
+
4
+ # Class to build complex FMP queries
5
+ # Perform Filemaker find using complex boolean logic (multiple value options for a single field)
6
+ # Or create multiple find requests.
7
+ # Also allow find requests to be :omit
8
+ class CompoundQuery < Array # @private :nodoc:
9
+
10
+ attr_accessor :original_input, :query_type, :key_values, :key_arrays, :key_map, :key_map_string, :key_counter
11
+
12
+ def self.build_test
13
+ new([{:field1=>['val1a','val1b','val1c'], :field2=>'val2'},{:omit=>true, :field3=>'val3', :field4=>'val4'}, {:omit=>true, :field5=>['val5a','val5b'], :field6=>['val6a','val6b']}], {})
14
+ end
15
+
16
+ # New CompoundQuery objects expect one of 3 data types:
17
+ # * string/integer representing FMP internal record id
18
+ # * hash of find-criteria
19
+ # * array of find-criteria hashes
20
+ #
21
+ # Returns self as ['-fmpaction', {:hash=>'of', :key=>'values'}, {:options=>'hash'}]
22
+ def initialize(query, options={})
23
+ @options = options
24
+ @original_input = query
25
+ @key_values = {}
26
+ @key_arrays = []
27
+ @key_map = []
28
+ @key_map_string = ''
29
+ @key_counter = 0
30
+
31
+ case query
32
+ when Hash
33
+ if query.detect{|k,v| v.kind_of? Array or k == :omit}
34
+ @query_type = 'mixed'
35
+ else
36
+ @query_type = 'standard'
37
+ end
38
+ when Array
39
+ @query_type = 'compound'
40
+ else
41
+ @query_type = 'recid'
42
+ end
43
+ build_query
44
+ end
45
+
46
+ # Master control method to build output
47
+ def build_query(input=original_input)
48
+ case @query_type
49
+ when 'mixed', 'compound'
50
+ input.rfm_force_array.each do |hash|
51
+ build_key_map(build_key_values(hash))
52
+ end
53
+ translate_key_map
54
+ self.push '-findquery'
55
+ self.push @key_values.merge('-query'=>@key_map_string)
56
+ when 'standard'
57
+ self.push '-find'
58
+ self.push @original_input
59
+ when 'recid'
60
+ self.push '-find'
61
+ self.push '-recid' => @original_input.to_s
62
+ end
63
+ self.push @options
64
+ self
65
+ end
66
+
67
+
68
+ # Build key-value definitions and query map '-q1...'
69
+ # Converts query_hash to fmresultset uri format for -findquery query type.
70
+ def build_key_values(input_hash)
71
+ input_hash = input_hash.clone
72
+ keyarray = []
73
+ omit = input_hash.delete(:omit)
74
+ input_hash.each do |key,val|
75
+ query_tag = []
76
+ val = val.rfm_force_array
77
+ val.each do |v|
78
+ @key_values["-q#{key_counter}"] = key
79
+ @key_values["-q#{key_counter}.value"] = v
80
+ query_tag << "q#{key_counter}"
81
+ @key_counter += 1
82
+ end
83
+ keyarray << query_tag
84
+ end
85
+ (keyarray << :omit) if omit
86
+ @key_arrays << keyarray
87
+ keyarray
88
+ end
89
+
90
+
91
+ # Input array of arrays.
92
+ # Transform single key_array into key_map (array of requests)
93
+ # Creates all combinations of sub-arrays where each combination contains one element of each subarray.
94
+ def build_key_map(key_array)
95
+ key_array = key_array.clone
96
+ omit = key_array.delete(:omit)
97
+ len = key_array.length
98
+ flat = key_array.flatten
99
+ rslt = flat.combination(len).select{|c| key_array.all?{|a| (a & c).size > 0}}.each{|c| c.unshift(:omit) if omit}
100
+ @key_map.concat rslt
101
+ end
102
+
103
+ # Translate @key_map to FMP -query string
104
+ def translate_key_map(keymap=key_map)
105
+ keymap = keymap.clone
106
+ inner = keymap.collect {|a| "#{'!' if a.delete(:omit)}(#{a.join(',')})"}
107
+ outter = inner.join(';')
108
+ @key_map_string << outter
109
+ end
110
+
111
+ end # CompoundQuery
112
+
113
+ end # Rfm
@@ -5,18 +5,35 @@ module Rfm
5
5
  # The subsets can be any grouping of defined config parameters, as a hash.
6
6
  # See CONFIG_KEYS for defined config parameters.
7
7
  #
8
- # All filters are honored, unless filters are included in calls to get_config,
9
- # in which case only the immediately specified filters will be used.
10
- #
11
- # Do not put a :use=>:group filter in a subset (maybe future feature?).
12
- # Do not put a :use=>:group filter in the top-level global parameters.
13
- # Do not put subsets in non-top-level configs. (maybe future feature?)
14
- #
15
8
  module Config
16
- CONFIG_KEYS = %w(parser host port account_name password database layout ssl root_cert root_cert_name root_cert_path warn_on_redirect raise_on_401 timeout log_actions log_responses log_parser use parent)
9
+ require 'yaml'
10
+
11
+ CONFIG_KEYS = %w(
12
+ file_name
13
+ file_path
14
+ parser
15
+ host
16
+ port
17
+ account_name
18
+ password
19
+ database
20
+ layout
21
+ ssl
22
+ root_cert
23
+ root_cert_name
24
+ root_cert_path
25
+ warn_on_redirect
26
+ raise_on_401
27
+ timeout
28
+ log_actions
29
+ log_responses
30
+ log_parser
31
+ use
32
+ parent
33
+ )
17
34
 
18
35
  extend self
19
- @config = {}
36
+ @config = {}
20
37
 
21
38
  # Set @config with args & options hash.
22
39
  # Args should be symbols representing configuration groups,
@@ -73,6 +90,22 @@ module Rfm
73
90
  end
74
91
 
75
92
  protected
93
+
94
+ # Get or load a config file as the top-level config (above RFM_CONFIG constant).
95
+ # Default file name is rfm.yml.
96
+ # Default paths are '' and 'config/'.
97
+ # File name & paths can be set in RFM_CONFIG and Rfm.config.
98
+ # Change file name with :file_name => 'something.else'
99
+ # Change file paths with :file_path => ['array/of/', 'file/paths/']
100
+ def get_config_file
101
+ @@config_file_data ||= (
102
+ config_file_name = @config[:file_name] || (RFM_CONFIG[:file_name] rescue nil) || 'rfm.yml'
103
+ config_file_paths = [''] | (@config[:file_path] || (RFM_CONFIG[:file_path] rescue nil) || %w( config/ ))
104
+ config_file_paths.collect do |f|
105
+ (YAML.load_file("#{f}#{config_file_name}") rescue {})
106
+ end.inject({}){|h,a| h.merge(a)}
107
+ ) || {}
108
+ end
76
109
 
77
110
 
78
111
  # Merge args into @config, as :use=>[arg1, arg2, ...]
@@ -91,21 +124,31 @@ module Rfm
91
124
  remote = if (self != Rfm::Config)
92
125
  eval(@config[:parent] || 'Rfm::Config').config_merge_with_parent rescue {}
93
126
  else
94
- (defined?(RFM_CONFIG) and RFM_CONFIG.is_a?(Hash)) ? RFM_CONFIG : {}
127
+ get_config_file.merge((defined?(RFM_CONFIG) and RFM_CONFIG.is_a?(Hash)) ? RFM_CONFIG : {})
95
128
  end
96
129
 
97
130
  use = (remote[:use].rfm_force_array | @config[:use].rfm_force_array).compact
98
131
  remote.merge(@config).merge(:use=>use)
99
- end
100
-
101
- # Given config hash, return filtered. Filters should be symbols.
132
+ end
133
+
134
+ # This version uses either/or method input filters OR compiled config :use=>filters.
135
+ # Given config hash, return filtered subgroup settings. Filters should be symbols.
136
+ # def config_filter(conf, filters=nil)
137
+ # filters ||= conf[:use].rfm_force_array if !conf[:use].blank?
138
+ # filters.each{|f| next unless conf[f]; conf.merge!(conf[f] || {})} if !filters.blank?
139
+ # conf.reject!{|k,v| !CONFIG_KEYS.include?(k.to_s) or v.to_s == '' }
140
+ # conf
141
+ # end
142
+ #
143
+ # This version combines both method input filters AND :use=>filters
102
144
  def config_filter(conf, filters=nil)
103
- filters ||= conf[:use].rfm_force_array if !conf[:use].blank?
145
+ filters = conf[:use] = (conf[:use].rfm_force_array | filters.rfm_force_array).compact
104
146
  filters.each{|f| next unless conf[f]; conf.merge!(conf[f] || {})} if !filters.blank?
105
- conf.reject!{|k,v| !CONFIG_KEYS.include?(k.to_s) or v.to_s == '' }
147
+ conf.reject!{|k,v| !CONFIG_KEYS.include?(k.to_s) or [{},[],''].include?(v) }
106
148
  conf
107
149
  end
108
-
150
+
151
+
109
152
  # This loads RFM_CONFIG into @config. It is not necessary,
110
153
  # as get_config will merge all configuration into RFM_CONFIG at runtime.
111
154
  #config RFM_CONFIG if defined? RFM_CONFIG