ginjo-rfm 1.4.4 → 2.0.pre31
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/CHANGELOG.md +107 -0
- data/README.md +378 -133
- data/lib/rfm.rb +51 -19
- data/lib/rfm/VERSION +1 -1
- data/lib/rfm/base.rb +416 -0
- data/lib/rfm/database.rb +14 -9
- data/lib/rfm/layout.rb +148 -96
- data/lib/rfm/metadata/field.rb +5 -5
- data/lib/rfm/metadata/field_control.rb +52 -51
- data/lib/rfm/metadata/script.rb +7 -5
- data/lib/rfm/record.rb +71 -56
- data/lib/rfm/resultset.rb +45 -26
- data/lib/rfm/server.rb +21 -17
- data/lib/rfm/utilities/complex_query.rb +64 -0
- data/lib/rfm/utilities/config.rb +115 -0
- data/lib/rfm/utilities/core_ext.rb +90 -0
- data/lib/rfm/utilities/factory.rb +100 -17
- data/lib/rfm/utilities/xml_parser.rb +94 -0
- data/lib/rfm/version.rb +1 -1
- data/lib/rfm/xml_mini/hpricot.rb +133 -0
- metadata +87 -30
data/lib/rfm/database.rb
CHANGED
@@ -65,17 +65,21 @@ module Rfm
|
|
65
65
|
# myDatabase = myServer["Customers"]
|
66
66
|
#
|
67
67
|
# This sample code gets a database object representing the Customers database on the FileMaker server.
|
68
|
-
def initialize(name,
|
69
|
-
|
70
|
-
@
|
71
|
-
|
72
|
-
@
|
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 == ''
|
70
|
+
@name = name.to_s
|
71
|
+
rfm_metaclass.instance_variable_set :@server, server_obj
|
72
|
+
@account_name = acnt #server.state[:account_name] or ""
|
73
|
+
@password = pass #server.state[:password] or ""
|
73
74
|
@layout = Rfm::Factory::LayoutFactory.new(server, self)
|
74
75
|
@script = Rfm::Factory::ScriptFactory.new(server, self)
|
75
76
|
end
|
76
77
|
|
77
|
-
|
78
|
+
meta_attr_reader :server
|
79
|
+
#attr_reader :server
|
80
|
+
attr_reader :name, :account_name, :password, :layout, :script
|
78
81
|
attr_writer :account_name, :password
|
82
|
+
alias_method :layouts, :layout
|
79
83
|
|
80
84
|
# Access the Layout object representing a layout in this database. For example:
|
81
85
|
#
|
@@ -88,9 +92,10 @@ module Rfm
|
|
88
92
|
# returned is created on the fly and assumed to refer to a valid layout, but you will
|
89
93
|
# get no error at this point if the layout you specify doesn't exist. Instead, you'll
|
90
94
|
# receive an error when you actually try to perform some action it.
|
91
|
-
|
92
|
-
|
93
|
-
|
95
|
+
# def [](layout_name)
|
96
|
+
# self.layout[layout_name]
|
97
|
+
# end
|
98
|
+
def_delegator :layout, :[]
|
94
99
|
|
95
100
|
end
|
96
101
|
end
|
data/lib/rfm/layout.rb
CHANGED
@@ -119,6 +119,8 @@ module Rfm
|
|
119
119
|
# list that is attached to any field on the layout
|
120
120
|
|
121
121
|
class Layout
|
122
|
+
|
123
|
+
include ComplexQuery
|
122
124
|
|
123
125
|
# Initialize a layout object. You never really need to do this. Instead, just do this:
|
124
126
|
#
|
@@ -133,127 +135,182 @@ module Rfm
|
|
133
135
|
#
|
134
136
|
# myServer = Rfm::Server.new(...)
|
135
137
|
# myLayout = myServer["Customers"]["Details"]
|
136
|
-
def initialize(name,
|
137
|
-
|
138
|
-
@
|
138
|
+
def initialize(name, db_obj)
|
139
|
+
raise Rfm::Error::RfmError.new(0, "New instance of Rfm::Layout has no name.") if name.to_s == ''
|
140
|
+
@name = name.to_s
|
141
|
+
rfm_metaclass.instance_variable_set :@db, db_obj
|
139
142
|
|
140
143
|
@loaded = false
|
141
144
|
@field_controls = Rfm::CaseInsensitiveHash.new
|
142
|
-
@value_lists = Rfm::CaseInsensitiveHash.new
|
145
|
+
@value_lists = Rfm::CaseInsensitiveHash.new
|
146
|
+
# @portal_meta = nil
|
147
|
+
# @field_names = nil
|
143
148
|
end
|
144
149
|
|
145
|
-
|
150
|
+
meta_attr_reader :db
|
151
|
+
attr_reader :name #, :db
|
152
|
+
attr_writer :field_names, :portal_meta, :table
|
153
|
+
def_delegator :db, :server
|
154
|
+
alias_method :database, :db
|
146
155
|
|
147
|
-
# Returns a ResultSet object containing _every record_ in the table associated with this layout.
|
148
|
-
def all(options = {})
|
149
|
-
get_records('-findall', {}, options)
|
150
|
-
end
|
151
156
|
|
152
|
-
#
|
153
|
-
|
154
|
-
|
155
|
-
end
|
156
|
-
|
157
|
-
# Finds a record. Typically you will pass in a hash of field names and values. For example:
|
158
|
-
#
|
159
|
-
# myLayout.find({"First Name" => "Bill"})
|
160
|
-
#
|
161
|
-
# Values in the hash work just like value in FileMaker's Find mode. You can use any special
|
162
|
-
# symbols (+==+, +...+, +>+, etc...).
|
163
|
-
#
|
164
|
-
# If you pass anything other than a hash as the first parameter, it is converted to a string and
|
165
|
-
# assumed to be FileMaker's internal id for a record (the recid).
|
166
|
-
def find(hash_or_recid, options = {})
|
167
|
-
if hash_or_recid.kind_of? Hash
|
168
|
-
get_records('-find', hash_or_recid, options)
|
169
|
-
else
|
170
|
-
get_records('-find', {'-recid' => hash_or_recid.to_s}, options)
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
# Updates the contents of the record whose internal +recid+ is specified. Send in a hash of new
|
175
|
-
# data in the +values+ parameter. Returns a RecordSet containing the modified record. For example:
|
176
|
-
#
|
177
|
-
# recid = myLayout.find({"First Name" => "Bill"})[0].record_id
|
178
|
-
# myLayout.edit(recid, {"First Name" => "Steve"})
|
179
|
-
#
|
180
|
-
# The above code would find the first record with _Bill_ in the First Name field and change the
|
181
|
-
# first name to _Steve_.
|
182
|
-
def edit(recid, values, options = {})
|
183
|
-
get_records('-edit', {'-recid' => recid}.merge(values), options)
|
184
|
-
end
|
157
|
+
# These methods are to be inclulded in Layout and SubLayout, so that
|
158
|
+
# they have their own descrete 'self' in the master class and the subclass.
|
159
|
+
module LayoutModule
|
185
160
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
161
|
+
# Returns a ResultSet object containing _every record_ in the table associated with this layout.
|
162
|
+
def all(options = {})
|
163
|
+
get_records('-findall', {}, options)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns a ResultSet containing a single random record from the table associated with this layout.
|
167
|
+
def any(options = {})
|
168
|
+
get_records('-findany', {}, options)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Finds a record. Typically you will pass in a hash of field names and values. For example:
|
172
|
+
#
|
173
|
+
# myLayout.find({"First Name" => "Bill"})
|
174
|
+
#
|
175
|
+
# Values in the hash work just like value in FileMaker's Find mode. You can use any special
|
176
|
+
# symbols (+==+, +...+, +>+, etc...).
|
177
|
+
#
|
178
|
+
# If you pass anything other than a hash as the first parameter, it is converted to a string and
|
179
|
+
# 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
|
186
|
+
end
|
187
|
+
|
188
|
+
# Updates the contents of the record whose internal +recid+ is specified. Send in a hash of new
|
189
|
+
# data in the +values+ parameter. Returns a RecordSet containing the modified record. For example:
|
190
|
+
#
|
191
|
+
# recid = myLayout.find({"First Name" => "Bill"})[0].record_id
|
192
|
+
# myLayout.edit(recid, {"First Name" => "Steve"})
|
193
|
+
#
|
194
|
+
# The above code would find the first record with _Bill_ in the First Name field and change the
|
195
|
+
# first name to _Steve_.
|
196
|
+
def edit(recid, values, options = {})
|
197
|
+
get_records('-edit', {'-recid' => recid}.merge(values), options)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Creates a new record in the table associated with this layout. Pass field data as a hash in the
|
201
|
+
# +values+ parameter. Returns the newly created record in a RecordSet. You can use the returned
|
202
|
+
# record to, ie, discover the values in auto-enter fields (like serial numbers).
|
203
|
+
#
|
204
|
+
# For example:
|
205
|
+
#
|
206
|
+
# result = myLayout.create({"First Name" => "Jerry", "Last Name" => "Robin"})
|
207
|
+
# id = result[0]["ID"]
|
208
|
+
#
|
209
|
+
# The above code adds a new record with first name _Jerry_ and last name _Robin_. It then
|
210
|
+
# puts the value from the ID field (a serial number) into a ruby variable called +id+.
|
211
|
+
def create(values, options = {})
|
212
|
+
get_records('-new', values, options)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Deletes the record with the specified internal recid. Returns a ResultSet with the deleted record.
|
216
|
+
#
|
217
|
+
# For example:
|
218
|
+
#
|
219
|
+
# recid = myLayout.find({"First Name" => "Bill"})[0].record_id
|
220
|
+
# myLayout.delete(recid)
|
221
|
+
#
|
222
|
+
# The above code finds every record with _Bill_ in the First Name field, then deletes the first one.
|
223
|
+
def delete(recid, options = {})
|
224
|
+
get_records('-delete', {'-recid' => recid}, options)
|
225
|
+
return nil
|
226
|
+
end
|
227
|
+
|
228
|
+
def get_records(action, extra_params = {}, options = {})
|
229
|
+
include_portals = options[:include_portals] ? options.delete(:include_portals) : nil
|
230
|
+
xml_response = db.server.connect(db.account_name, db.password, action, params.merge(extra_params), options).body
|
231
|
+
Rfm::Resultset.new(db.server, xml_response, self, include_portals)
|
232
|
+
end
|
233
|
+
|
234
|
+
def params
|
235
|
+
{"-db" => db.name, "-lay" => self.name}
|
236
|
+
end
|
237
|
+
|
238
|
+
end # LayoutModule
|
239
|
+
include LayoutModule
|
200
240
|
|
201
|
-
# Deletes the record with the specified internal recid. Returns a ResultSet with the deleted record.
|
202
|
-
#
|
203
|
-
# For example:
|
204
|
-
#
|
205
|
-
# recid = myLayout.find({"First Name" => "Bill"})[0].record_id
|
206
|
-
# myLayout.delete(recid)
|
207
|
-
#
|
208
|
-
# The above code finds every record with _Bill_ in the First Name field, then deletes the first one.
|
209
|
-
def delete(recid, options = {})
|
210
|
-
get_records('-delete', {'-recid' => recid}, options)
|
211
|
-
return nil
|
212
|
-
end
|
213
241
|
|
214
242
|
def field_controls
|
215
243
|
load unless @loaded
|
216
244
|
@field_controls
|
217
245
|
end
|
218
246
|
|
247
|
+
def field_names
|
248
|
+
load unless @field_names
|
249
|
+
@field_names
|
250
|
+
end
|
251
|
+
|
252
|
+
def field_names_no_load
|
253
|
+
@field_names
|
254
|
+
end
|
255
|
+
|
219
256
|
def value_lists
|
220
257
|
load unless @loaded
|
221
258
|
@value_lists
|
222
259
|
end
|
223
260
|
|
224
|
-
|
261
|
+
def total_count
|
262
|
+
any.total_count
|
263
|
+
end
|
264
|
+
|
265
|
+
def portal_meta
|
266
|
+
@portal_meta ||= any.portal_meta
|
267
|
+
end
|
268
|
+
|
269
|
+
def portal_meta_no_load
|
270
|
+
@portal_meta
|
271
|
+
end
|
272
|
+
|
273
|
+
def portal_names
|
274
|
+
portal_meta.keys
|
275
|
+
end
|
276
|
+
|
277
|
+
def table
|
278
|
+
@table ||= any.table
|
279
|
+
end
|
280
|
+
|
281
|
+
def table_no_load
|
282
|
+
@table
|
283
|
+
end
|
225
284
|
|
226
285
|
def load
|
227
|
-
#require 'rexml/document'
|
228
|
-
require 'nokogiri'
|
229
|
-
|
230
286
|
@loaded = true
|
231
|
-
fmpxmllayout =
|
232
|
-
doc =
|
287
|
+
fmpxmllayout = db.server.load_layout(self)
|
288
|
+
doc = XmlParser.new(fmpxmllayout.body, :namespace=>false, :parser=>server.state[:parser])
|
233
289
|
|
234
290
|
# check for errors
|
235
|
-
error = doc
|
291
|
+
error = doc['FMPXMLLAYOUT']['ERRORCODE'].to_s.to_i
|
236
292
|
raise Rfm::Error::FileMakerError.getError(error) if error != 0
|
237
293
|
|
238
294
|
# process valuelists
|
239
|
-
|
240
|
-
|
295
|
+
vlists = doc['FMPXMLLAYOUT']['VALUELISTS']['VALUELIST']
|
296
|
+
if !vlists.nil? #root.elements['VALUELISTS'].size > 0
|
297
|
+
vlists.each {|valuelist|
|
241
298
|
name = valuelist['NAME']
|
242
|
-
@value_lists[name] = valuelist.
|
243
|
-
Rfm::Metadata::ValueListItem.new(value
|
299
|
+
@value_lists[name] = valuelist['VALUE'].collect{|value|
|
300
|
+
Rfm::Metadata::ValueListItem.new(value['__content__'], value['DISPLAY'], name)
|
244
301
|
} rescue []
|
245
302
|
}
|
246
303
|
@value_lists.freeze
|
247
304
|
end
|
248
305
|
|
249
306
|
# process field controls
|
250
|
-
doc
|
307
|
+
doc['FMPXMLLAYOUT']['LAYOUT']['FIELD'].each {|field|
|
251
308
|
name = field['NAME']
|
252
|
-
|
253
|
-
|
254
|
-
value_list_name =
|
309
|
+
style = field['STYLE']
|
310
|
+
type = style['TYPE']
|
311
|
+
value_list_name = style['VALUELIST']
|
255
312
|
value_list = @value_lists[value_list_name] if value_list_name != ''
|
256
|
-
field_control = Rfm::Metadata::FieldControl.new(name,
|
313
|
+
field_control = Rfm::Metadata::FieldControl.new(name, type, value_list_name, value_list)
|
257
314
|
existing = @field_controls[name]
|
258
315
|
if existing
|
259
316
|
if existing.kind_of?(Array)
|
@@ -265,17 +322,12 @@ module Rfm
|
|
265
322
|
@field_controls[name] = field_control
|
266
323
|
end
|
267
324
|
}
|
268
|
-
@field_controls.
|
269
|
-
|
270
|
-
|
271
|
-
def get_records(action, extra_params = {}, options = {})
|
272
|
-
include_portals = options[:include_portals] ? options.delete(:include_portals) : nil
|
273
|
-
xml_response = @db.server.connect(@db.account_name, @db.password, action, params.merge(extra_params), options).body
|
274
|
-
Rfm::Resultset.new(@db.server, xml_response, self, include_portals)
|
325
|
+
@field_names ||= @field_controls.collect{|k,v| v.name rescue v[0].name}
|
326
|
+
@field_controls.freeze
|
275
327
|
end
|
276
328
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
end
|
281
|
-
end
|
329
|
+
private :load, :get_records, :params
|
330
|
+
|
331
|
+
|
332
|
+
end # Layout
|
333
|
+
end # Rfm
|
data/lib/rfm/metadata/field.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module Rfm
|
2
2
|
module Metadata
|
3
|
+
|
3
4
|
# The Field object represents a single FileMaker field. It *does not hold the data* in the field. Instead,
|
4
5
|
# it serves as a source of metadata about the field. For example, if you're script is trying to be highly
|
5
6
|
# dynamic about its field access, it may need to determine the data type of a field at run time. Here's
|
@@ -56,7 +57,6 @@ module Rfm
|
|
56
57
|
#
|
57
58
|
# The code above makes sure the control is always an array. Typically, though, you'll know up front
|
58
59
|
# if the control is an array or not, and you can code accordingly.
|
59
|
-
|
60
60
|
class Field
|
61
61
|
|
62
62
|
attr_reader :name, :result, :type, :max_repeats, :global
|
@@ -75,7 +75,7 @@ module Rfm
|
|
75
75
|
# type of the field. You'll never need to do this: Rfm does it automatically for you when you
|
76
76
|
# access field data through the Record object.
|
77
77
|
def coerce(value, resultset)
|
78
|
-
return nil if value.empty?
|
78
|
+
return nil if (value.nil? or value.empty?)
|
79
79
|
case self.result
|
80
80
|
when "text" then value
|
81
81
|
when "number" then BigDecimal.new(value)
|
@@ -88,6 +88,6 @@ module Rfm
|
|
88
88
|
|
89
89
|
end
|
90
90
|
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
91
|
+
end # Field
|
92
|
+
end # Metadata
|
93
|
+
end # Rfm
|
@@ -1,53 +1,54 @@
|
|
1
1
|
module Rfm
|
2
|
-
module Metadata
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
2
|
+
module Metadata
|
3
|
+
|
4
|
+
# The FieldControl object represents a field on a FileMaker layout. You can find out what field
|
5
|
+
# style the field uses, and the value list attached to it.
|
6
|
+
#
|
7
|
+
# =Attributes
|
8
|
+
#
|
9
|
+
# * *name* is the name of the field
|
10
|
+
#
|
11
|
+
# * *style* is any one of:
|
12
|
+
# * * :edit_box - a normal editable field
|
13
|
+
# * * :scrollable - an editable field with scroll bar
|
14
|
+
# * * :popup_menu - a pop-up menu
|
15
|
+
# * * :checkbox_set - a set of checkboxes
|
16
|
+
# * * :radio_button_set - a set of radio buttons
|
17
|
+
# * * :popup_list - a pop-up list
|
18
|
+
# * * :calendar - a pop-up calendar
|
19
|
+
#
|
20
|
+
# * *value_list_name* is the name of the attached value list, if any
|
21
|
+
#
|
22
|
+
# * *value_list* is an array of strings representing the value list items, or nil
|
23
|
+
# if this field has no attached value list
|
24
|
+
class FieldControl
|
25
|
+
def initialize(name, style, value_list_name, value_list)
|
26
|
+
@name = name
|
27
|
+
case style
|
28
|
+
when "EDITTEXT"
|
29
|
+
@style = :edit_box
|
30
|
+
when "POPUPMENU"
|
31
|
+
@style = :popup_menu
|
32
|
+
when "CHECKBOX"
|
33
|
+
@style = :checkbox_set
|
34
|
+
when "RADIOBUTTONS"
|
35
|
+
@style = :radio_button_set
|
36
|
+
when "POPUPLIST"
|
37
|
+
@style = :popup_list
|
38
|
+
when "CALENDAR"
|
39
|
+
@style = :calendar
|
40
|
+
when "SCROLLTEXT"
|
41
|
+
@style = :scrollable
|
42
|
+
else
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
@value_list_name = value_list_name
|
46
|
+
rfm_metaclass.instance_variable_set :@value_list, value_list
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :name, :style, :value_list_name
|
50
|
+
meta_attr_reader :value_list
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
53
54
|
end
|