fm_store 0.1
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/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
|