fm_store 0.1

Sign up to get free protection for your applications and to get access to all the features.
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