lardawge-rfm 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rfm_command.rb DELETED
@@ -1,790 +0,0 @@
1
- require 'net/https'
2
- require 'cgi'
3
- require 'nokogiri'
4
-
5
- # This module includes classes that represent base FileMaker concepts like servers,
6
- # layouts, and scripts. These classes allow you to communicate with FileMaker Server,
7
- # send commands, and receive responses.
8
- #
9
- # Author:: Geoff Coffey (mailto:gwcoffey@gmail.com)
10
- # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
11
- # License:: See MIT-LICENSE for details
12
- module Rfm
13
-
14
- # This class represents a single FileMaker server. It is initialized with basic
15
- # connection information, including the hostname, port number, and default database
16
- # account name and password.
17
- #
18
- # Note: The host and port number refer to the FileMaker Web Publishing Engine, which
19
- # must be installed and configured in order to use RFM. It may not actually be running
20
- # on the same server computer as FileMaker Server itself. See your FileMaker Server
21
- # or FileMaker Server Advanced documentation for information about configuring a Web
22
- # Publishing Engine.
23
- #
24
- # =Accessing Databases
25
- #
26
- # Typically, you access a Database object from the Server like this:
27
- #
28
- # myDatabase = myServer["Customers"]
29
- #
30
- # This code gets the Database object representing the Customers object.
31
- #
32
- # Note: RFM does not talk to the server when you retrieve a database object in this way. Instead, it
33
- # simply assumes you know what you're talking about. If the database you specify does not exist, you
34
- # will get no error at this point. Instead, you'll get an error when you use the Layout object you get
35
- # from this database. This makes debugging a little less convenient, but it would introduce too much
36
- # overhead to hit the server at this point.
37
- #
38
- # The Server object has a +db+ attribute that provides alternate access to Database objects. It acts
39
- # like a hash of Database objects, one for each accessible database on the server. So, for example, you
40
- # can do this if you want to print out a list of all databses on the server:
41
- #
42
- # myServer.db.each {|database|
43
- # puts database.name
44
- # }
45
- #
46
- # The Server::db attribute is actually a DbFactory object, although it subclasses hash, so it should work
47
- # in all the ways you expect. Note, though, that it is completely empty until the first time you attempt
48
- # to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of databases, and
49
- # constructs a Database object for each one. In other words, it incurrs no overhead until you use it.
50
- #
51
- # =Attributes
52
- #
53
- # In addition to the +db+ attribute, Server has a few other useful attributes:
54
- #
55
- # * *host_name* is the host name this server points to
56
- # * *port* is the port number this server communicates on
57
- # * *state* is a hash of all server options used to initialize this server
58
-
59
- class Server
60
-
61
- #
62
- # SSL AND CERTIFICATE VERIFICATION ARE ON BY DEFAULT
63
- #
64
- # Example to turn off SSL:
65
- #
66
- # response = myServer.do_action(
67
- # :host => 'localhost',
68
- # :account_name => 'sample',
69
- # :password => '12345',
70
- # :ssl => false
71
- # )
72
- #
73
- # Example using SSL without *root_cert*:
74
- #
75
- # response = myServer.do_action(
76
- # :host => 'localhost',
77
- # :account_name => 'sample',
78
- # :password => '12345',
79
- # :root_cert => false
80
- # )
81
- #
82
- # Example using SSL with *root_cert* at file root:
83
- #
84
- # response = myServer.do_action(
85
- # :host => 'localhost',
86
- # :account_name => 'sample',
87
- # :password => '12345',
88
- # :root_cert_name => 'example.pem'
89
- # )
90
- #
91
- # Example using SSL with *root_cert* specifying *root_cert_path*:
92
- #
93
- # response = myServer.do_action(
94
- # :host => 'localhost',
95
- # :account_name => 'sample',
96
- # :password => '12345',
97
- # :root_cert_name => 'example.pem'
98
- # :root_cert_path => '/usr/cert_file/'
99
- # )
100
- #
101
- # To create a Server obejct, you typically need at least a host name:
102
- #
103
- # myServer = Rfm::Server.new({:host => 'my.host.com'})
104
- #
105
- # Several other options are supported:
106
- #
107
- # * *host* the hostname of the Web Publishing Engine (WPE) server (defaults to 'localhost')
108
- #
109
- # * *port* the port number the WPE is listening no (defaults to 80 unless *ssl* +true+ which sets it to 443)
110
- #
111
- # * *account_name* the default account name to log in to databases with (you can also supply a
112
- # account name on a per-database basis if necessary)
113
- #
114
- # * *password* the default password to log in to databases with (you can also supplly a password
115
- # on a per-databases basis if necessary)
116
- #
117
- # * *log_actions* when +true+, RFM logs all action URLs that are sent to FileMaker server to stderr
118
- # (defaults to +false+)
119
- #
120
- # * *log_responses* when +true+, RFM logs all raw XML responses (including headers) from FileMaker to
121
- # stderr (defaults to +false+)
122
- #
123
- # * *warn_on_redirect* normally, RFM prints a warning to stderr if the Web Publishing Engine redirects
124
- # (this can usually be fixed by using a different host name, which speeds things up); if you *don't*
125
- # want this warning printed, set +warn_on_redirect+ to +true+
126
- #
127
- # * *raise_on_401* although RFM raises error when FileMaker returns error responses, it typically
128
- # ignores FileMaker's 401 error (no records found) and returns an empty record set instead; if you
129
- # prefer a raised error when a find produces no errors, set this option to +true+
130
- #
131
- #SSL Options:
132
- #
133
- # * *ssl* +false+ if you want to turn SSL (HTTPS) off when connecting to connect to FileMaker (default is +true+)
134
- #
135
- # If you are using SSL on and want to verify the certificate use the following options:
136
- #
137
- # * *root_cert* +false+ if you do not want to verify your SSL session (default is +true+).
138
- # You will want to turn this off if you are using a self signed certificate and do not have a certificate authority cert file.
139
- # If you choose this option you will need to provide a cert *root_cert_name* and *root_cert_path* (if not in root directory).
140
- #
141
- # * *root_cert_name* name of pem file for certificate verification (Root cert from certificate authority who issued certificate.
142
- # If self signed certificate do not use this option!!). You can download the entire bundle of CA Root Certificates
143
- # from http://curl.haxx.se/ca/cacert.pem. Place the pem file in config directory.
144
- #
145
- # * *root_cert_path* path to cert file. (defaults to '/' if no path given)
146
-
147
- def initialize(options)
148
- @state = {
149
- :host => 'localhost',
150
- :port => 80,
151
- :ssl => true,
152
- :root_cert => true,
153
- :root_cert_name => '',
154
- :root_cert_path => '/',
155
- :account_name => '',
156
- :password => '',
157
- :log_actions => false,
158
- :log_responses => false,
159
- :warn_on_redirect => true,
160
- :raise_on_401 => false
161
- }.merge(options)
162
-
163
- if @state[:username] != nil
164
- warn("the :username option on Rfm::Server::initialize has been deprecated. Use :account_name instead.")
165
- @state[:account_name] = @state[:username]
166
- end
167
-
168
- @state.freeze
169
-
170
- @host_name = @state[:host]
171
- @scheme = @state[:ssl] ? "https" : "http"
172
- @port = @state[:ssl] && options[:port].nil? ? 443 : @state[:port]
173
-
174
- @db = Rfm::Factory::DbFactory.new(self)
175
- end
176
-
177
- # Access the database object representing a database on the server. For example:
178
- #
179
- # myServer['Customers']
180
- #
181
- # would return a Database object representing the _Customers_
182
- # database on the server.
183
- #
184
- # Note: RFM never talks to the server until you perform an action. The database object
185
- # returned is created on the fly and assumed to refer to a valid database, but you will
186
- # get no error at this point if the database you access doesn't exist. Instead, you'll
187
- # receive an error when you actually try to perform some action on a layout from this
188
- # database.
189
- def [](dbname)
190
- self.db[dbname]
191
- end
192
-
193
- attr_reader :db, :host_name, :port, :scheme, :state
194
-
195
- # Performs a raw FileMaker action. You will generally not call this method directly, but it
196
- # is exposed in case you need to do something "under the hood."
197
- #
198
- # The +action+ parameter is any valid FileMaker web url action. For example, +-find+, +-finadny+ etc.
199
- #
200
- # The +args+ parameter is a hash of arguments to be included in the action url. It will be serialized
201
- # and url-encoded appropriately.
202
- #
203
- # The +options+ parameter is a hash of RFM-specific options, which correspond to the more esoteric
204
- # FileMaker URL parameters. They are exposed separately because they can also be passed into
205
- # various methods on the Layout object, which is a much more typical way of sending an action to
206
- # FileMaker.
207
- #
208
- # This method returns the Net::HTTP response object representing the response from FileMaker.
209
- #
210
- # For example, if you wanted to send a raw command to FileMaker to find the first 20 people in the
211
- # "Customers" database whose first name is "Bill" you might do this:
212
- #
213
- # response = myServer.do_action(
214
- # '-find',
215
- # {
216
- # "-db" => "Customers",
217
- # "-lay" => "Details",
218
- # "First Name" => "Bill"
219
- # },
220
- # { :max_records => 20 }
221
- # )
222
- def do_action(account_name, password, action, args, options = {})
223
- post = args.merge(expand_options(options)).merge({action => ''})
224
- http_fetch(@host_name, @port, "/fmi/xml/fmresultset.xml", account_name, password, post)
225
- end
226
-
227
- def load_layout(layout)
228
- post = {'-db' => layout.db.name, '-lay' => layout.name, '-view' => ''}
229
- http_fetch(@host_name, @port, "/fmi/xml/FMPXMLLAYOUT.xml", layout.db.account_name, layout.db.password, post)
230
- end
231
-
232
- private
233
-
234
- def http_fetch(host_name, port, path, account_name, password, post_data, limit = 10)
235
- if limit == 0
236
- raise Rfm::Error::CommunicationError.new("While trying to reach the Web Publishing Engine, RFM was redirected too many times.")
237
- end
238
-
239
- if @state[:log_actions] == true
240
- qs = post_data.collect{|key,val| "#{CGI::escape(key.to_s)}=#{CGI::escape(val.to_s)}"}.join("&")
241
- warn "#{@scheme}://#{@host_name}:#{@port}#{path}?#{qs}"
242
- end
243
-
244
- request = Net::HTTP::Post.new(path)
245
- request.basic_auth(account_name, password)
246
- request.set_form_data(post_data)
247
-
248
- response = Net::HTTP.new(host_name, port)
249
- if @scheme == "https" # enable SSL
250
- response.use_ssl = true
251
- if @state[:root_cert]
252
- response.verify_mode = OpenSSL::SSL::VERIFY_PEER
253
- response.ca_file = File.join(@state[:root_cert_path], @state[:root_cert_name])
254
- else
255
- response.verify_mode = OpenSSL::SSL::VERIFY_NONE
256
- end
257
- end
258
- response = response.start { |http|
259
- http.request(request)
260
- }
261
-
262
- if @state[:log_responses] == true
263
- response.to_hash.each {|key, value|
264
- warn "#{key}: #{value}"
265
- }
266
- warn response.body
267
- end
268
-
269
- case response
270
- when Net::HTTPSuccess
271
- response
272
- when Net::HTTPRedirection
273
- if @state[:warn_on_redirect]
274
- warn "The web server redirected to " + response['location'] + ". You should revise your connection hostname or fix your server configuration if possible to improve performance."
275
- end
276
- newloc = URI.parse(response['location'])
277
- http_fetch(newloc.host, newloc.port, newloc.request_uri, account_name, password, post_data, limit - 1)
278
- when Net::HTTPUnauthorized
279
- msg = "The account name (#{account_name}) or password provided is not correct (or the account doesn't have the fmxml extended privilege)."
280
- raise Rfm::Error::AuthenticationError.new(msg)
281
- when Net::HTTPNotFound
282
- msg = "Could not talk to FileMaker because the Web Publishing Engine is not responding (server returned 404)."
283
- raise Rfm::Error::CommunicationError.new(msg)
284
- else
285
- msg = "Unexpected response from server: #{result.code} (#{result.class.to_s}). Unable to communicate with the Web Publishing Engine."
286
- raise Rfm::Error::CommunicationError.new(msg)
287
- end
288
- end
289
-
290
- def expand_options(options)
291
- result = {}
292
- options.each {|key,value|
293
- case key
294
- when :max_records:
295
- result['-max'] = value
296
- when :skip_records:
297
- result['-skip'] = value
298
- when :sort_field:
299
- if value.kind_of? Array
300
- if value.size > 9
301
- raise Rfm::Error::ParameterError.new(":sort_field can have at most 9 fields, but you passed an array with #{value.size} elements.")
302
- end
303
- value.each_index {|i|
304
- result["-sortfield.#{i+1}"] = value[i]
305
- }
306
- else
307
- result["-sortfield.1"] = value
308
- end
309
- when :sort_order:
310
- if value.kind_of? Array
311
- if value.size > 9
312
- raise Rfm::Error::ParameterError.new(":sort_order can have at most 9 fields, but you passed an array with #{value.size} elements.")
313
- end
314
- value.each_index {|i|
315
- result["-sortorder.#{i+1}"] = value[i]
316
- }
317
- else
318
- result["-sortorder.1"] = value
319
- end
320
- when :post_script:
321
- if value.class == Array
322
- result['-script'] = value[0]
323
- result['-script.param'] = value[1]
324
- else
325
- result['-script'] = value
326
- end
327
- when :pre_find_script:
328
- if value.class == Array
329
- result['-script.prefind'] = value[0]
330
- result['-script.prefind.param'] = value[1]
331
- else
332
- result['-script.presort'] = value
333
- end
334
- when :pre_sort_script:
335
- if value.class == Array
336
- result['-script.presort'] = value[0]
337
- result['-script.presort.param'] = value[1]
338
- else
339
- result['-script.presort'] = value
340
- end
341
- when :response_layout:
342
- result['-lay.response'] = value
343
- when :logical_operator:
344
- result['-lop'] = value
345
- when :modification_id:
346
- result['-modid'] = value
347
- else
348
- raise Rfm::Error::ParameterError.new("Invalid option: #{key} (are you using a string instead of a symbol?)")
349
- end
350
- }
351
- result
352
- end
353
-
354
- end
355
-
356
- # The Database object represents a single FileMaker Pro database. When you retrieve a Database
357
- # object from a server, its account name and password are set to the account name and password you
358
- # used when initializing the Server object. You can override this of course:
359
- #
360
- # myDatabase = myServer["Customers"]
361
- # myDatabase.account_name = "foo"
362
- # myDatabase.password = "bar"
363
- #
364
- # =Accessing Layouts
365
- #
366
- # All interaction with FileMaker happens through a Layout object. You can get a Layout object
367
- # from the Database object like this:
368
- #
369
- # myLayout = myDatabase["Details"]
370
- #
371
- # This code gets the Layout object representing the layout called Details in the database.
372
- #
373
- # Note: RFM does not talk to the server when you retrieve a Layout object in this way. Instead, it
374
- # simply assumes you know what you're talking about. If the layout you specify does not exist, you
375
- # will get no error at this point. Instead, you'll get an error when you use the Layout object methods
376
- # to talk to FileMaker. This makes debugging a little less convenient, but it would introduce too much
377
- # overhead to hit the server at this point.
378
- #
379
- # The Database object has a +layout+ attribute that provides alternate access to Layout objects. It acts
380
- # like a hash of Layout objects, one for each accessible layout in the database. So, for example, you
381
- # can do this if you want to print out a list of all layouts:
382
- #
383
- # myDatabase.layout.each {|layout|
384
- # puts layout.name
385
- # }
386
- #
387
- # The Database::layout attribute is actually a LayoutFactory object, although it subclasses hash, so it
388
- # should work in all the ways you expect. Note, though, that it is completely empty until the first time
389
- # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of layouts,
390
- # and constructs a Layout object for each one. In other words, it incurrs no overhead until you use it.
391
- #
392
- # =Accessing Scripts
393
- #
394
- # If for some reason you need to enumerate the scripts in a database, you can do so:
395
- #
396
- # myDatabase.script.each {|script|
397
- # puts script.name
398
- # }
399
- #
400
- # The Database::script attribute is actually a ScriptFactory object, although it subclasses hash, so it
401
- # should work in all the ways you expect. Note, though, that it is completely empty until the first time
402
- # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of scripts,
403
- # and constructs a Script object for each one. In other words, it incurrs no overhead until you use it.
404
- #
405
- # Note: You don't need a Script object to _run_ a script (see the Layout object instead).
406
- #
407
- # =Attributes
408
- #
409
- # In addition to the +layout+ attribute, Server has a few other useful attributes:
410
- #
411
- # * *server* is the Server object this database comes from
412
- # * *name* is the name of this database
413
- # * *state* is a hash of all server options used to initialize this server
414
- class Database
415
-
416
- # Initialize a database object. You never really need to do this. Instead, just do this:
417
- #
418
- # myServer = Rfm::Server.new(...)
419
- # myDatabase = myServer["Customers"]
420
- #
421
- # This sample code gets a database object representing the Customers database on the FileMaker server.
422
- def initialize(name, server)
423
- @name = name
424
- @server = server
425
- @account_name = server.state[:account_name] or ""
426
- @password = server.state[:password] or ""
427
- @layout = Rfm::Factory::LayoutFactory.new(server, self)
428
- @script = Rfm::Factory::ScriptFactory.new(server, self)
429
- end
430
-
431
- attr_reader :server, :name, :account_name, :password, :layout, :script
432
- attr_writer :account_name, :password
433
-
434
- # Access the Layout object representing a layout in this database. For example:
435
- #
436
- # myDatabase['Details']
437
- #
438
- # would return a Layout object representing the _Details_
439
- # layout in the database.
440
- #
441
- # Note: RFM never talks to the server until you perform an action. The Layout object
442
- # returned is created on the fly and assumed to refer to a valid layout, but you will
443
- # get no error at this point if the layout you specify doesn't exist. Instead, you'll
444
- # receive an error when you actually try to perform some action it.
445
- def [](layout_name)
446
- self.layout[layout_name]
447
- end
448
-
449
- end
450
-
451
- # The Layout object represents a single FileMaker Pro layout. You use it to interact with
452
- # records in FileMaker. *All* access to FileMaker data is done through a layout, and this
453
- # layout determins which _table_ you actually hit (since every layout is explicitly associated
454
- # with a particular table -- see FileMakers Layout->Layout Setup dialog box). You never specify
455
- # _table_ information directly in RFM.
456
- #
457
- # Also, the layout determines which _fields_ will be returned. If a layout contains only three
458
- # fields from a large table, only those three fields are returned. If a layout includes related
459
- # fields from another table, they are returned as well. And if the layout includes portals, all
460
- # data in the portals is returned (see Record::portal for details).
461
- #
462
- # As such, you can _significantly_ improve performance by limiting what you put on the layout.
463
- #
464
- # =Using Layouts
465
- #
466
- # The Layout object is where you get most of your work done. It includes methods for all
467
- # FileMaker actions:
468
- #
469
- # * Layout::all
470
- # * Layout::any
471
- # * Layout::find
472
- # * Layout::edit
473
- # * Layout::create
474
- # * Layout::delete
475
- #
476
- # =Running Scripts
477
- #
478
- # In FileMaker, execution of a script must accompany another action. For example, to run a script
479
- # called _Remove Duplicates_ with a found set that includes everybody
480
- # named _Bill_, do this:
481
- #
482
- # myLayout.find({"First Name" => "Bill"}, :post_script => "Remove Duplicates")
483
- #
484
- # ==Controlling When the Script Runs
485
- #
486
- # When you perform an action in FileMaker, it always executes in this order:
487
- #
488
- # 1. Perform any find
489
- # 2. Sort the records
490
- # 3. Return the results
491
- #
492
- # You can control when in the process the script runs. Each of these options is available:
493
- #
494
- # * *post_script* tells FileMaker to run the script after finding and sorting
495
- # * *pre_find_script* tells FileMaker to run the script _before_ finding
496
- # * *pre_sort_script* tells FileMaker to run the script _before_ sorting, but _after_ finding
497
- #
498
- # ==Passing Parameters to a Script
499
- #
500
- # If you want to pass a parameter to the script, use the options above, but supply an array value
501
- # instead of a single string. For example:
502
- #
503
- # myLayout.find({"First Name" => "Bill"}, :post_script => ["Remove Duplicates", 10])
504
- #
505
- # This sample runs the script called "Remove Duplicates" and passes it the value +10+ as its
506
- # script parameter.
507
- #
508
- # =Common Options
509
- #
510
- # Most of the methods on the Layout object accept an optional hash of +options+ to manipulate the
511
- # action. For example, when you perform a find, you will typiclaly get back _all_ matching records.
512
- # If you want to limit the number of records returned, you can do this:
513
- #
514
- # myLayout.find({"First Name" => "Bill"}, :max_records => 100)
515
- #
516
- # The +:max_records+ option tells FileMaker to limit the number of records returned.
517
- #
518
- # This is the complete list of available options:
519
- #
520
- # * *max_records* tells FileMaker how many records to return
521
- #
522
- # * *skip_records* tells FileMaker how many records in the found set to skip, before
523
- # returning results; this is typically combined with +max_records+ to "page" through
524
- # records
525
- #
526
- # * *sort_field* tells FileMaker to sort the records by the specified field
527
- #
528
- # * *sort_order* can be +descend+ or +ascend+ and determines the order
529
- # of the sort when +sort_field+ is specified
530
- #
531
- # * *post_script* tells FileMaker to perform a script after carrying out the action; you
532
- # can pass the script name, or a two-element array, with the script name first, then the
533
- # script parameter
534
- #
535
- # * *pre_find_script* is like +post_script+ except the script runs before any find is
536
- # performed
537
- #
538
- # * *pre_sort_script* is like +pre_find_script+ except the script runs after any find
539
- # and before any sort
540
- #
541
- # * *response_layout* tells FileMaker to switch layouts before producing the response; this
542
- # is useful when you need a field on a layout to perform a find, edit, or create, but you
543
- # want to improve performance by not including the field in the result
544
- #
545
- # * *logical_operator* can be +and+ or +or+ and tells FileMaker how to process multiple fields
546
- # in a find request
547
- #
548
- # * *modification_id* lets you pass in the modification id from a Record object with the request;
549
- # when you do, the action will fail if the record was modified in FileMaker after it was retrieved
550
- # by RFM but before the action was run
551
- #
552
- #
553
- # =Attributes
554
- #
555
- # The Layout object has a few useful attributes:
556
- #
557
- # * +name+ is the name of the layout
558
- #
559
- # * +field_controls+ is a hash of FieldControl objects, with the field names as keys. FieldControl's
560
- # tell you about the field on the layout: how is it formatted and what value list is assigned
561
- #
562
- # Note: It is possible to put the same field on a layout more than once. When this is the case, the
563
- # value in +field_controls+ for that field is an array with one element representing each instance
564
- # of the field.
565
- #
566
- # * +value_lists+ is a hash of arrays. The keys are value list names, and the values in the hash
567
- # are arrays containing the actual value list items. +value_lists+ will include every value
568
- # list that is attached to any field on the layout
569
-
570
- class Layout
571
-
572
- # Initialize a layout object. You never really need to do this. Instead, just do this:
573
- #
574
- # myServer = Rfm::Server.new(...)
575
- # myDatabase = myServer["Customers"]
576
- # myLayout = myDatabase["Details"]
577
- #
578
- # This sample code gets a layout object representing the Details layout in the Customers database
579
- # on the FileMaker server.
580
- #
581
- # In case it isn't obvious, this is more easily expressed this way:
582
- #
583
- # myServer = Rfm::Server.new(...)
584
- # myLayout = myServer["Customers"]["Details"]
585
- def initialize(name, db)
586
- @name = name
587
- @db = db
588
-
589
- @loaded = false
590
- @field_controls = Rfm::Util::CaseInsensitiveHash.new
591
- @value_lists = Rfm::Util::CaseInsensitiveHash.new
592
- end
593
-
594
- attr_reader :name, :db
595
-
596
- def field_controls
597
- load if !@loaded
598
- @field_controls
599
- end
600
-
601
- def value_lists
602
- load if !@loaded
603
- @value_lists
604
- end
605
-
606
- # Returns a ResultSet object containing _every record_ in the table associated with this layout.
607
- def all(options = {})
608
- get_records('-findall', {}, options)
609
- end
610
-
611
- # Returns a ResultSet containing a single random record from the table associated with this layout.
612
- def any(options = {})
613
- get_records('-findany', {}, options)
614
- end
615
-
616
- # Finds a record. Typically you will pass in a hash of field names and values. For example:
617
- #
618
- # myLayout.find({"First Name" => "Bill"})
619
- #
620
- # Values in the hash work just like value in FileMaker's Find mode. You can use any special
621
- # symbols (+==+, +...+, +>+, etc...).
622
- #
623
- # If you pass anything other than a hash as the first parameter, it is converted to a string and
624
- # assumed to be FileMaker's internal id for a record (the recid).
625
- def find(hash_or_recid, options = {})
626
- if hash_or_recid.kind_of? Hash
627
- get_records('-find', hash_or_recid, options)
628
- else
629
- get_records('-find', {'-recid' => hash_or_recid.to_s}, options)
630
- end
631
- end
632
-
633
- # Updates the contents of the record whose internal +recid+ is specified. Send in a hash of new
634
- # data in the +values+ parameter. Returns a RecordSet containing the modified record. For example:
635
- #
636
- # recid = myLayout.find({"First Name" => "Bill"})[0].record_id
637
- # myLayout.edit(recid, {"First Name" => "Steve"})
638
- #
639
- # The above code would find the first record with _Bill_ in the First Name field and change the
640
- # first name to _Steve_.
641
- def edit(recid, values, options = {})
642
- get_records('-edit', {'-recid' => recid}.merge(values), options)
643
- end
644
-
645
- # Creates a new record in the table associated with this layout. Pass field data as a hash in the
646
- # +values+ parameter. Returns the newly created record in a RecordSet. You can use the returned
647
- # record to, ie, discover the values in auto-enter fields (like serial numbers).
648
- #
649
- # For example:
650
- #
651
- # result = myLayout.create({"First Name" => "Jerry", "Last Name" => "Robin"})
652
- # id = result[0]["ID"]
653
- #
654
- # The above code adds a new record with first name _Jerry_ and last name _Robin_. It then
655
- # puts the value from the ID field (a serial number) into a ruby variable called +id+.
656
- def create(values, options = {})
657
- get_records('-new', values, options)
658
- end
659
-
660
- # Deletes the record with the specified internal recid. Returns a ResultSet with the deleted record.
661
- #
662
- # For example:
663
- #
664
- # recid = myLayout.find({"First Name" => "Bill"})[0].record_id
665
- # myLayout.delete(recid)
666
- #
667
- # The above code finds every record with _Bill_ in the First Name field, then deletes the first one.
668
- def delete(recid, options = {})
669
- get_records('-delete', {'-recid' => recid}, options)
670
- return nil
671
- end
672
-
673
- private
674
-
675
- def load
676
- @loaded = true
677
- fmpxmllayout = @db.server.load_layout(self).body
678
- doc = REXML::Document.new(fmpxmllayout)
679
- root = doc.root
680
-
681
- # check for errors
682
- error = root.elements['ERRORCODE'].text.to_i
683
- raise Rfm::Error::FileMakerError.getError(error) if error != 0
684
-
685
- # process valuelists
686
- if root.elements['VALUELISTS'].size > 0
687
- root.elements['VALUELISTS'].each_element('VALUELIST') { |valuelist|
688
- name = valuelist.attributes['NAME']
689
- @value_lists[name] = valuelist.elements.collect {|e| e.text}
690
- }
691
- @value_lists.freeze
692
- end
693
-
694
- # process field controls
695
- root.elements['LAYOUT'].each_element('FIELD') { |field|
696
- name = field.attributes['NAME']
697
- style = field.elements['STYLE'].attributes['TYPE']
698
- value_list_name = field.elements['STYLE'].attributes['VALUELIST']
699
- value_list = @value_lists[value_list_name] if value_list_name != ''
700
- field_control = FieldControl.new(name, style, value_list_name, value_list)
701
- existing = @field_controls[name]
702
- if existing
703
- if existing.kind_of?(Array)
704
- existing << field_control
705
- else
706
- @field_controls[name] = Array[existing, field_control]
707
- end
708
- else
709
- @field_controls[name] = field_control
710
- end
711
- }
712
- @field_controls.freeze
713
- end
714
-
715
- def get_records(action, extra_params = {}, options = {})
716
- Rfm::Result::ResultSet.new(
717
- @db.server, @db.server.do_action(@db.account_name, @db.password, action, params().merge(extra_params), options).body,
718
- self)
719
- end
720
-
721
- def params
722
- {"-db" => @db.name, "-lay" => self.name}
723
- end
724
- end
725
-
726
- # The FieldControl object represents a field on a FileMaker layout. You can find out what field
727
- # style the field uses, and the value list attached to it.
728
- #
729
- # =Attributes
730
- #
731
- # * *name* is the name of the field
732
- #
733
- # * *style* is any one of:
734
- # * * :edit_box - a normal editable field
735
- # * * :scrollable - an editable field with scroll bar
736
- # * * :popup_menu - a pop-up menu
737
- # * * :checkbox_set - a set of checkboxes
738
- # * * :radio_button_set - a set of radio buttons
739
- # * * :popup_list - a pop-up list
740
- # * * :calendar - a pop-up calendar
741
- #
742
- # * *value_list_name* is the name of the attached value list, if any
743
- #
744
- # * *value_list* is an array of strings representing the value list items, or nil
745
- # if this field has no attached value list
746
- class FieldControl
747
- def initialize(name, style, value_list_name, value_list)
748
- @name = name
749
- case style
750
- when "EDITTEXT"
751
- @style = :edit_box
752
- when "POPUPMENU"
753
- @style = :popup_menu
754
- when "CHECKBOX"
755
- @style = :checkbox_set
756
- when "RADIOBUTTONS"
757
- @style = :radio_button_set
758
- when "POPUPLIST"
759
- @style = :popup_list
760
- when "CALENDAR"
761
- @style = :calendar
762
- when "SCROLLTEXT"
763
- @style = :scrollable
764
- end
765
- @value_list_name = value_list_name
766
- @value_list = value_list
767
- end
768
-
769
- attr_reader :name, :style, :value_list_name, :value_list
770
-
771
- end
772
-
773
- # The Script object represents a FileMaker script. At this point, the Script object exists only so
774
- # you can enumrate all scripts in a Database (which is a rare need):
775
- #
776
- # myDatabase.script.each {|script|
777
- # puts script.name
778
- # }
779
- #
780
- # If you want to _run_ a script, see the Layout object instead.
781
- class Script
782
- def initialize(name, db)
783
- @name = name
784
- @db = db
785
- end
786
-
787
- attr_reader :name
788
- end
789
-
790
- end