fm_store 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +13 -0
- data/README.rdoc +211 -0
- data/lib/fm_store.rb +35 -0
- data/lib/fm_store/associations.rb +44 -0
- data/lib/fm_store/associations/belongs_to.rb +36 -0
- data/lib/fm_store/associations/has_many.rb +39 -0
- data/lib/fm_store/associations/options.rb +30 -0
- data/lib/fm_store/associations/proxy.rb +17 -0
- data/lib/fm_store/builders/collection.rb +33 -0
- data/lib/fm_store/builders/single.rb +25 -0
- data/lib/fm_store/components.rb +19 -0
- data/lib/fm_store/config.rb +19 -0
- data/lib/fm_store/connection.rb +23 -0
- data/lib/fm_store/criteria.rb +98 -0
- data/lib/fm_store/criterion/exclusion.rb +24 -0
- data/lib/fm_store/criterion/inclusion.rb +255 -0
- data/lib/fm_store/criterion/optional.rb +46 -0
- data/lib/fm_store/ext/field.rb +20 -0
- data/lib/fm_store/field.rb +14 -0
- data/lib/fm_store/fields.rb +36 -0
- data/lib/fm_store/finders.rb +36 -0
- data/lib/fm_store/layout.rb +156 -0
- data/lib/fm_store/paging.rb +27 -0
- data/lib/fm_store/persistence.rb +109 -0
- data/lib/fm_store/railtie.rb +17 -0
- data/lib/fm_store/version.rb +4 -0
- data/lib/generators/fm_model/fm_model_generator.rb +28 -0
- data/lib/generators/fm_model/templates/model.rb +10 -0
- metadata +95 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Criterion
|
4
|
+
module Optional
|
5
|
+
# Tell FileMaker how many records in the found set to skip.
|
6
|
+
# Use together with +limit+ to page through the records
|
7
|
+
def skip(value = 20)
|
8
|
+
@options[:skip_records] = value
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def limit(value = 20)
|
13
|
+
@options[:max_records] = value
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
# +ascend+ or +descend+
|
18
|
+
def order(field_and_orders)
|
19
|
+
sorts = field_and_orders.split(",").map(&:strip)
|
20
|
+
sort_field = []
|
21
|
+
sort_order = []
|
22
|
+
|
23
|
+
sorts.each do |s|
|
24
|
+
field, order = s.split(" ")
|
25
|
+
order = "asc" unless order
|
26
|
+
|
27
|
+
fm_name = klass.find_fm_name(field)
|
28
|
+
|
29
|
+
order = "ascend" if order.downcase == "asc"
|
30
|
+
order = "descend" if order.downcase == "desc"
|
31
|
+
|
32
|
+
sort_field << fm_name
|
33
|
+
sort_order << order
|
34
|
+
end
|
35
|
+
|
36
|
+
@options[:sort_field] = sort_field
|
37
|
+
@options[:sort_order] = sort_order
|
38
|
+
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# logical_operator
|
43
|
+
# modification_id
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rfm
|
2
|
+
module Metadata
|
3
|
+
class Field
|
4
|
+
def coerce(value, resultset)
|
5
|
+
return nil if (value.nil? || value.empty?)
|
6
|
+
case self.result
|
7
|
+
when "text" then value
|
8
|
+
when "number" then BigDecimal.new(value)
|
9
|
+
when "date" then Date.strptime(value, resultset.date_format)
|
10
|
+
when "time" then DateTime.strptime("1/1/-4712 #{value}", "%m/%d/%Y #{resultset.time_format}")
|
11
|
+
when "timestamp" then DateTime.strptime(value, resultset.timestamp_format)
|
12
|
+
when "container" then URI.parse("#{resultset.server.scheme}://#{resultset.server.host_name}:#{resultset.server.port}#{value}")
|
13
|
+
else nil
|
14
|
+
end
|
15
|
+
rescue
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
class Field
|
4
|
+
attr_reader :name, :type, :fm_name, :searchable, :identity
|
5
|
+
|
6
|
+
def initialize(name, type, options = {})
|
7
|
+
@name = name
|
8
|
+
@fm_name = options[:fm_name] || name
|
9
|
+
@type = type
|
10
|
+
@searchable = options[:searchable] || false
|
11
|
+
@identity = options[:identity] || false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Fields
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_inheritable_accessor :fields
|
8
|
+
|
9
|
+
self.fields = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Defines all the fields that are available from the layout.
|
14
|
+
def field(name, type, options = {})
|
15
|
+
set_field(name.to_s, type, options)
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def set_field(name, type, options)
|
21
|
+
# We key FileMaker name rather then user specified name
|
22
|
+
fields[options[:fm_name] || name] = Field.new(name, type, options)
|
23
|
+
create_accessors(name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_accessors(name)
|
27
|
+
define_method(name) { instance_variable_get("@#{name}") }
|
28
|
+
define_method("#{name}=") { |value| instance_variable_set("@#{name}", value) }
|
29
|
+
define_method("#{name}?") do
|
30
|
+
attr = instance_variable_get("@#{name}")
|
31
|
+
(@type == Boolean) ? attr == true : attr.present?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Finders
|
4
|
+
|
5
|
+
# Criteria
|
6
|
+
[:where, :limit, :skip, :order, :exclude, :search, :id, :fm_id].each do |name|
|
7
|
+
define_method(name) do |*args|
|
8
|
+
criteria.send(name, *args)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
[:in, :custom_query].each do |name|
|
13
|
+
define_method(name) do |*args|
|
14
|
+
criteria_query.send(name, *args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def criteria
|
19
|
+
Criteria.new(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def criteria_query
|
23
|
+
Criteria.new(self, true)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Will always return an array properly cast to the correct type
|
27
|
+
# def find(hash_or_record_id, options = {})
|
28
|
+
# where(hash_or_record_id, options)
|
29
|
+
# end
|
30
|
+
|
31
|
+
def total
|
32
|
+
criteria.paginate(:per_page => 1).total_entries
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Layout
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include FmStore::Components
|
8
|
+
self.include_root_in_json = false
|
9
|
+
|
10
|
+
cattr_accessor :layout, :database
|
11
|
+
|
12
|
+
attr_reader :new_record, :record_id, :mod_id
|
13
|
+
|
14
|
+
define_model_callbacks :create, :save, :update, :validation, :destroy
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def set_layout(layout)
|
19
|
+
self.layout = layout
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_database(database)
|
23
|
+
self.database = database
|
24
|
+
end
|
25
|
+
|
26
|
+
# Calling self.fields will ideally match here
|
27
|
+
# See FieldControl
|
28
|
+
def fm_fields
|
29
|
+
conn = Connection.establish_connection(self)
|
30
|
+
rs = conn.any.first.keys.inspect
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return the real FileMaker, nil otherwise
|
34
|
+
def find_fm_name(attribute_name)
|
35
|
+
if fields.has_key?(attribute_name)
|
36
|
+
return attribute_name
|
37
|
+
else
|
38
|
+
f = fields.find { |a| a.last.name == attribute_name }
|
39
|
+
|
40
|
+
f.last.fm_name if f
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_fm_type(attribute_name)
|
45
|
+
f = fields.find { |a| a.last.name == attribute_name }
|
46
|
+
|
47
|
+
f.last.type if f
|
48
|
+
end
|
49
|
+
|
50
|
+
def searchable_fields
|
51
|
+
fields.map(&:last).select(&:searchable).map(&:name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def identity
|
55
|
+
fields.map(&:last).find(&:identity).try(:fm_name) || "-recid"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Drop-down, for example
|
59
|
+
# http://host/fmi/xml/FMPXMLLAYOUT.xml?-db=jobs+&-lay=jobs&-view=
|
60
|
+
def value_lists
|
61
|
+
conn = Connection.establish_connection(self)
|
62
|
+
conn.value_lists
|
63
|
+
end
|
64
|
+
|
65
|
+
def first
|
66
|
+
limit(1).first
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(attributes = {})
|
71
|
+
@associations = {}
|
72
|
+
@new_record = true
|
73
|
+
process(attributes)
|
74
|
+
end
|
75
|
+
|
76
|
+
def fm_attributes
|
77
|
+
attrs = {}
|
78
|
+
|
79
|
+
fields.each do |fm_attr, field|
|
80
|
+
ivar = send("#{field.name}")
|
81
|
+
|
82
|
+
type = field.type
|
83
|
+
|
84
|
+
if type == Date
|
85
|
+
ivar = ivar.strftime("%m/%d/%Y") if ivar
|
86
|
+
elsif type == DateTime
|
87
|
+
ivar = ivar.strftime("%m/%d/%Y %H:%M:%S") if ivar
|
88
|
+
elsif type == Time
|
89
|
+
ivar = ivar.strftime("%H:%M") if ivar
|
90
|
+
end
|
91
|
+
|
92
|
+
# case ivar
|
93
|
+
# when Date
|
94
|
+
# ivar = ivar.strftime("%m/%d/%Y")
|
95
|
+
# when DateTime
|
96
|
+
# ivar = ivar.strftime("%m/%d/%Y %H:%M:%S")
|
97
|
+
# when Time
|
98
|
+
# ivar = ivar.strftime("%H:%M")
|
99
|
+
# end
|
100
|
+
|
101
|
+
attrs[fm_attr] = ivar if ivar # ignore nil attributes
|
102
|
+
end
|
103
|
+
|
104
|
+
attrs
|
105
|
+
end
|
106
|
+
|
107
|
+
def attributes
|
108
|
+
@attributes = {}
|
109
|
+
|
110
|
+
fields.each do |fm_attr, field|
|
111
|
+
ivar = send("#{field.name}")
|
112
|
+
@attributes[field.name] = ivar
|
113
|
+
end
|
114
|
+
|
115
|
+
return @attributes
|
116
|
+
end
|
117
|
+
|
118
|
+
def reload
|
119
|
+
@associations = {}
|
120
|
+
self.class.id(id)
|
121
|
+
end
|
122
|
+
|
123
|
+
def id
|
124
|
+
self.class.identity == "-recid" ? @record_id : send(self.class.fields[self.class.identity].name)
|
125
|
+
end
|
126
|
+
|
127
|
+
def new_record?
|
128
|
+
@new_record
|
129
|
+
end
|
130
|
+
|
131
|
+
def to_param
|
132
|
+
id.to_s if id
|
133
|
+
end
|
134
|
+
|
135
|
+
# Require by ActiveModel
|
136
|
+
def to_model
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_key
|
141
|
+
id if id
|
142
|
+
end
|
143
|
+
|
144
|
+
def persisted?
|
145
|
+
!new_record?
|
146
|
+
end
|
147
|
+
|
148
|
+
protected
|
149
|
+
|
150
|
+
def process(attributes)
|
151
|
+
attributes.each do |k, v|
|
152
|
+
send("#{k}=", v) if respond_to?(k)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Paging
|
4
|
+
def paginate(opts = {})
|
5
|
+
options[:max_records] = opts[:per_page] || 30
|
6
|
+
|
7
|
+
if opts[:page]
|
8
|
+
options[:skip_records] = (opts[:page].to_i - 1) * options[:max_records].to_i
|
9
|
+
end
|
10
|
+
|
11
|
+
collection = execute(true)
|
12
|
+
|
13
|
+
WillPaginate::Collection.create(page, per_page, count) do |pager|
|
14
|
+
pager.replace(collection)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def page
|
19
|
+
skips, limits = options[:skip_records], options[:max_records]
|
20
|
+
(skips && limits) ? (skips + limits) / limits : 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def per_page
|
24
|
+
(options[:max_records] || 30).to_i
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Persistence
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# The place where all the persistence took place, like insert, update
|
7
|
+
module ClassMethods
|
8
|
+
def create(attributes = {})
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Instance methods
|
14
|
+
def save
|
15
|
+
create_or_update
|
16
|
+
end
|
17
|
+
|
18
|
+
def update_attributes(attributes = {})
|
19
|
+
if valid?
|
20
|
+
attrs = {}
|
21
|
+
|
22
|
+
attributes.each do |field, value|
|
23
|
+
field = field.to_s
|
24
|
+
|
25
|
+
fm_name = self.class.find_fm_name(field)
|
26
|
+
type = self.class.find_fm_type(field)
|
27
|
+
|
28
|
+
if fm_name
|
29
|
+
if type == Date
|
30
|
+
if value.blank?
|
31
|
+
value = '' # clear the date
|
32
|
+
else
|
33
|
+
value = value.strftime("%m/%d/%Y")
|
34
|
+
end
|
35
|
+
elsif type == DateTime
|
36
|
+
if value.blank?
|
37
|
+
value = ''
|
38
|
+
else
|
39
|
+
value = value.strftime("%m/%d/%Y %H:%M:%S")
|
40
|
+
end
|
41
|
+
elsif type == Time
|
42
|
+
if value.blank?
|
43
|
+
value = ''
|
44
|
+
else
|
45
|
+
value = value.strftime("%H:%M")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
attrs[fm_name] = value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
run_callbacks(:save) do
|
54
|
+
conn = Connection.establish_connection(self.class)
|
55
|
+
result = conn.edit(@record_id, attrs)
|
56
|
+
|
57
|
+
return FmStore::Builders::Single.build(result, self.class)
|
58
|
+
end;self # just in case
|
59
|
+
else
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Throws Rfm::Error::RecordAccessDeniedError if no permission to delete
|
65
|
+
def destroy
|
66
|
+
run_callbacks(:destroy) do
|
67
|
+
unless @record_id.nil?
|
68
|
+
conn = Connection.establish_connection(self.class)
|
69
|
+
conn.delete(@record_id)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
alias :delete :destroy
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
# Will always return +self+
|
79
|
+
def create_or_update
|
80
|
+
result = new_record? ? create : update
|
81
|
+
end
|
82
|
+
|
83
|
+
def create
|
84
|
+
if valid?
|
85
|
+
run_callbacks(:save) do
|
86
|
+
conn = Connection.establish_connection(self.class)
|
87
|
+
result = conn.create(self.fm_attributes)
|
88
|
+
|
89
|
+
@record_id = result[0].record_id
|
90
|
+
@new_record = false
|
91
|
+
end; self
|
92
|
+
else
|
93
|
+
false
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def update
|
98
|
+
if valid?
|
99
|
+
run_callbacks(:save) do
|
100
|
+
conn = Connection.establish_connection(self.class)
|
101
|
+
result = conn.edit(@record_id, self.fm_attributes)
|
102
|
+
end; self
|
103
|
+
else
|
104
|
+
false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|