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 ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2010 mech
2
+
3
+ Permission is hereby granted, to any person obtaining a copy of this software
4
+ and associated documentation files (the "Software"), including the rights to use,
5
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
6
+ Software.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
9
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
10
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
11
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
12
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
13
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,211 @@
1
+ == FmStore
2
+
3
+ Based on Mongoid implementation, FmStore behaves in a way very similar to how
4
+ you might expect ActiveRecord to behave. With callback and validation and ActiveModel
5
+ adherence.
6
+
7
+ == Database setup
8
+
9
+ Place a fm_store.yml file inside the config directory.
10
+
11
+ development:
12
+ host: 127.0.0.1
13
+ account_name:
14
+ password:
15
+ ssl: false
16
+ log_actions: true
17
+
18
+ == Model setup
19
+
20
+ You can setup a model using the following generator:
21
+
22
+ rails generate fm_model <file_name> <layout_name> <database_name>
23
+
24
+ This is using the Job example:
25
+
26
+ class Job
27
+ include FmStore::Layout
28
+
29
+ set_layout "jobs"
30
+ set_database "jobs "
31
+
32
+ field :modify_date, DateTime, :fm_name => "modify date"
33
+ field :company, String
34
+ field :location, String
35
+ field :jdid, String
36
+ ... # more fields
37
+
38
+ has_many :job_applications, :reference_key => "jdid"
39
+
40
+ validates_presence_of :job_title
41
+
42
+ # put your class methods here
43
+ class << self
44
+ def published
45
+ where("status" => "open")
46
+ end
47
+ end
48
+ end
49
+
50
+ == format_with option
51
+
52
+ There will be times when FileMaker give us String or Numeric ID. For example, +caid+ can become "52383.0".
53
+ What we really want is a String like "52383". You can force it by using the +format_with+ option and provide
54
+ your own implementation of the ID.
55
+
56
+ has_many :employments, :reference_key => "candidate_id", :format_with => :formatted_candidate_id
57
+
58
+ def formatted_candidate_id
59
+ candidate_id.to_s.split(".").first
60
+ end
61
+
62
+ == Finders
63
+
64
+ All finder methods such as +where+, +limit+, +order+, +in+ return a criteria object
65
+ which contain the params and options. No database call will be made until a kicker
66
+ method has been called such as +each+, +first+, etc.
67
+
68
+ # Find single job with ID. There is no #find method. Not to be confused with
69
+ # the instance method Job#id like @job.id which return you "JDID1" for example.
70
+ # ID will default to -recid. If you wish to change that, use the :identity option
71
+ class Job
72
+ include FmStore::Layout
73
+
74
+ field :job_id, String, :fm_name => "jdid", :identity => true
75
+ end
76
+
77
+ @job = Job.id("JDID1") #=> You do not need to append "=" sign to it, like "=JDID1"
78
+
79
+ You still can get the original internal FileMaker id by:
80
+
81
+ @job = Job.fm_id("18") # Using the -recid
82
+
83
+ # Find 10 records
84
+ @jobs = Job.limit(10)
85
+
86
+ # Find 10 records sorted. By default is ASC
87
+ @jobs = Job.limit(10).order("status") # same as @jobs = Job.limit(10).order("status asc")
88
+ @jobs = Job.limit(10).order("status desc, jdid")
89
+
90
+ # Find based on condition (single value per field). Please use the field name
91
+ # rather than the FileMaker field name like "modify date"
92
+ @jobs = Job.where("status" => "open")
93
+ @jobs = Job.where("category" => "Account", "status" => "open")
94
+
95
+ # Find with operator
96
+ @jobs = Job.where("salary.gt" => 2500)
97
+
98
+ # Find all payroll details whose gross salary is between $1.00 to $10.00 in order
99
+ PayrollDetail.where("gross_salary.bw" => "1...10").order("gross salary")
100
+
101
+ # Excluding
102
+ # WARNING - exclude cannot chain to search for now
103
+ @jobs = Job.where("status.neq" => "open")
104
+ @jobs = Job.exclude("status" => "open") # this is preferred
105
+
106
+ # Logical OR
107
+ # By default, conditions are ANDed together. Pass in false to make it ORed together
108
+ # Remember to supply curly braces for the first parameter which is a hash
109
+ @jobs = Job.where({"status" => "open", :category => "Account"}, false)
110
+
111
+ # Total count
112
+ @total = Job.where("status" => "closed").total
113
+ @total = Job.total
114
+
115
+ # Find based on multiple values in a single field
116
+ @jobs = Job.in(:status => ["pending", "closed"])
117
+ @jobs = Job.in(:status => ["pending", "closed"]).order("status").limit(10)
118
+
119
+ # You can also pass in single String instead of an array
120
+ @jobs = Job.in(:status => "open", :job_id => ["=JD123", "=JD456"]) #=> the value "open" will automatically be ["open"]
121
+
122
+ == Search
123
+
124
+ Every model will be exposed the +search+ class method. Based on the +searchable+
125
+ field option, this +search+ method will know which fields to search for.
126
+
127
+ # In your model
128
+ field :name, String, :fm_name => "company", :searchable => true
129
+
130
+ # Search for it in your controller
131
+ @company = Company.search(params)
132
+
133
+ @companies = Company.where(:category => "REGULAR").search(params)
134
+ @jobs = Job.in(:status => ["open", "pending"]).search(:q => "engineer, programmer")
135
+
136
+ +search+ will always look for params[:q] as the query keyword and params[:page] as the page number.
137
+
138
+ WARNING - Please note that +exclude+ cannot use +search+ for now.
139
+
140
+ Some search examples:
141
+
142
+ Job.where(:status => "open").search(:q => "engineer") #=> (q0,q1)
143
+ Job.in(:status => ["open"]).search(:q => "engineer") #=> (q0,q1)
144
+ Job.in(:status => ["open", "pending"]).search(:q => "engineer") #=> (q0,q2);(q1,q2)
145
+
146
+ == Pagination
147
+
148
+ Paging is supported via WillPaginate rails3 branch. Any +limit+ criteria will be
149
+ ignore when you call +paginate+, but you can override +per_page+ as usual.
150
+
151
+ @jobs = Job.where("status" => "open").paginate(:page => params[:page] || 1)
152
+ @jobs = Job.where("status" => "open").paginate(:per_page => 10, :page => params[:page] || 1)
153
+
154
+ +paginate+ is a kicker method in itself so database connection will be made and
155
+ result being retrieved.
156
+
157
+ == Custom query
158
+
159
+ If any of the +where+, +in+, +exclude+ do not meet your need, you can build the query yourself using FileMaker's +findquery+ command.
160
+
161
+ For example:
162
+
163
+ JobApplication.custom_query(
164
+ "-query" => "(q0,q1);!(q2);!(q3);!(q4)",
165
+ "-q0" => "caid",
166
+ "-q0.value" => "123",
167
+ "-q1" => "status2",
168
+ "-q1.value" => "DeclineOffer",
169
+ "-q2" => "status1",
170
+ "-q2.value" => "Apply",
171
+ "-q3" => "status1",
172
+ "-q3.value" => "Select",
173
+ "-q4" => "status1",
174
+ "-q4.value" => "SMS"
175
+ ).order("date2 DESC")
176
+
177
+ The query is
178
+
179
+ (q0,q1);!(q2);!(q3);!(q4)
180
+
181
+ which means find
182
+
183
+ q0 and q1 and omit q2, q3, and q4
184
+
185
+ == Available operators
186
+
187
+ * eq =word
188
+ * cn *word*
189
+ * bw word*
190
+ * ew *word
191
+ * gt > word
192
+ * gte >= word
193
+ * lt < word
194
+ * lte <= word
195
+ * neq omit,word
196
+
197
+ == Persistence
198
+
199
+ In order to save data to FileMaker, check you have the necessary permission.
200
+
201
+ @leave = Leave.new
202
+ @leave.contract_code = "S1234.01"
203
+ @leave.fm_attributes #=> {"contract code" => "S1234.01"}
204
+ @leave.from_date = Date.today
205
+ @leave.fm_attributes #=> {"from"=>"08/19/2010", "contract code" => "S1234.01"}
206
+
207
+ @leave.valid? # test if model is valid or not
208
+ @leave.errors # get all the errors if there are any
209
+ @leave.save # this will automatically called valid? and return false if failed
210
+
211
+ As you can see, the real work of converting Ruby object to the one suitable for FileMaker is the +fm_attributes+ value method.
data/lib/fm_store.rb ADDED
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+ # Copyright (c) 2010 mech
3
+ #
4
+ require 'rubygems'
5
+
6
+ gem "activemodel", ">=3.0.3"
7
+ gem "will_paginate", "~>3.0.pre"
8
+
9
+ require "singleton"
10
+ require 'yaml'
11
+ require 'rfm'
12
+ require 'rfm/metadata/field'
13
+ require 'fm_store/ext/field'
14
+ require 'active_support'
15
+ require 'active_support/core_ext'
16
+ require 'active_support/notifications'
17
+ require 'active_model'
18
+ require 'will_paginate/collection'
19
+
20
+ require 'fm_store/config'
21
+ require 'fm_store/associations'
22
+ require 'fm_store/persistence'
23
+ require 'fm_store/builders/collection'
24
+ require 'fm_store/builders/single'
25
+ require 'fm_store/connection'
26
+ require 'fm_store/components'
27
+ require 'fm_store/field'
28
+ require 'fm_store/fields'
29
+ require 'fm_store/finders'
30
+ require 'fm_store/criteria'
31
+ require 'fm_store/layout'
32
+
33
+ if defined?(Rails)
34
+ require 'fm_store/railtie'
35
+ end
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+ require 'fm_store/associations/proxy'
3
+ require 'fm_store/associations/options'
4
+ require 'fm_store/associations/has_many'
5
+ require 'fm_store/associations/belongs_to'
6
+
7
+ module FmStore
8
+ module Associations
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+ def has_many(name, options = {})
13
+ associate(Associations::HasMany, optionize(name, options), false)
14
+ end
15
+
16
+ def belongs_to(name, options= {})
17
+ associate(Associations::BelongsTo, optionize(name, options))
18
+ end
19
+
20
+ protected
21
+
22
+ def associate(type, options, cached = true)
23
+ name = options.name.to_s
24
+
25
+ define_method(name) do
26
+ if cached
27
+ @associations[name] ||= type.new(self, options)
28
+ else
29
+ # do not cached those that returns criteria
30
+ type.new(self, options)
31
+ end
32
+ end
33
+
34
+ define_method("#{name}=") do |object|
35
+ type.update(object, self, options)
36
+ end
37
+ end
38
+
39
+ def optionize(name, options)
40
+ Associations::Options.new(options.merge(:name => name))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ # encoding: utf-8
2
+ module FmStore
3
+ module Associations
4
+ class BelongsTo < Proxy
5
+
6
+ attr_accessor :association_name, :klass
7
+
8
+ def initialize(layout, options)
9
+ @layout, @association_name = layout, options.name
10
+ @klass, @reference_key, @options = options.klass, options.reference_key, options
11
+ @format_with = options.format_with
12
+
13
+ build_parent
14
+ end
15
+
16
+ protected
17
+
18
+ def build_parent
19
+ if @format_with
20
+ if @layout.send(@format_with.to_sym).nil?
21
+ @target = nil
22
+ else
23
+ @target = @klass.where({@reference_key => "=#{@layout.send(@format_with.to_sym)}"}).limit(1).first
24
+ end
25
+ else
26
+ if @layout.send(@reference_key.to_sym).nil?
27
+ @target = nil
28
+ else
29
+ @target = @klass.where({@reference_key => "=#{@layout.send(@reference_key.to_sym)}"}).limit(1).first
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+ module FmStore
3
+ module Associations
4
+ class HasMany < Proxy
5
+
6
+ attr_accessor :association_name, :klass
7
+
8
+ # @layout is an instance of the model and not the class itself
9
+ def initialize(layout, options)
10
+ @layout, @association_name = layout, options.name
11
+ @klass, @reference_key, @options = options.klass, options.reference_key, options
12
+ @format_with = options.format_with
13
+
14
+ build_children
15
+ end
16
+
17
+ protected
18
+
19
+ def build_children
20
+ # Returns a criteria rather then grabbing the records, so we do not
21
+ # waste request trip
22
+ if @format_with
23
+ if @layout.send(@format_with.to_sym).nil?
24
+ @target = nil
25
+ else
26
+ @target = @klass.where({@reference_key => "=#{@layout.send(@format_with.to_sym)}"})
27
+ end
28
+ else
29
+ if @layout.send(@reference_key.to_sym).nil?
30
+ @target = nil
31
+ else
32
+ @target = @klass.where({@reference_key => "=#{@layout.send(@reference_key.to_sym)}"})
33
+ end
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+ module FmStore
3
+ module Associations
4
+ class Options
5
+ def initialize(attributes = {})
6
+ @attributes = attributes
7
+ end
8
+
9
+ def reference_key
10
+ @attributes[:reference_key]
11
+ end
12
+
13
+ def klass
14
+ class_name.constantize
15
+ end
16
+
17
+ def class_name
18
+ @attributes[:class_name] || name.to_s.classify
19
+ end
20
+
21
+ def name
22
+ @attributes[:name].to_s
23
+ end
24
+
25
+ def format_with
26
+ @attributes[:format_with]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+ module FmStore #:nodoc
3
+ module Associations
4
+ class Proxy
5
+ instance_methods.each do |method|
6
+ undef_method(method) unless method =~ /(^__|^nil\?$|^send$|^object_id$|^extend$)/
7
+ end
8
+
9
+ attr_reader :target, :options
10
+
11
+ def method_missing(name, *args, &block)
12
+ @target.send(name, *args, &block)
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+ module FmStore
3
+ module Builders
4
+ class Collection
5
+ # Build an array of native object (i.e. Job, Payroll, etc) from the
6
+ # FileMaker records.
7
+ #
8
+ # Pass in the desired <tt>model</tt>
9
+ def self.build(records, model)
10
+ target = []
11
+
12
+ records.each do |record|
13
+ fm_fields = record.keys
14
+
15
+ obj = model.new
16
+
17
+ fm_fields.each do |fm_field|
18
+ field = model.fields[fm_field] # Field
19
+ obj.instance_variable_set("@#{field.name}", record[fm_field]) if field.respond_to?(:name)
20
+ end
21
+
22
+ obj.instance_variable_set("@new_record", false)
23
+ obj.instance_variable_set("@mod_id", record.mod_id)
24
+ obj.instance_variable_set("@record_id", record.record_id)
25
+
26
+ target << obj
27
+ end
28
+
29
+ return target
30
+ end
31
+ end
32
+ end
33
+ end