ginjo-rfm 1.4.4 → 2.0.pre31
Sign up to get free protection for your applications and to get access to all the features.
- 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
|