ginjo-rfm 2.0.pre31 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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