motion_model 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ 2012-01-09: Added automatic date/timestamp support for created_at and updated_at columns
2
+ Added Hash extension except, Array introspection methods has_hash_key? and
3
+ has_hash_value?
4
+ Commit of Formotion module including optional inclusion/suppression of the
5
+ auto-date fields
6
+ Specs
7
+
8
+
1
9
  2012-12-30: Added Formotion module. This allows for tighter integration with Formotion
2
10
  Changed options for columns such that any arbitrary values can be inserted
3
11
  allowing for future expansion.
data/README.md CHANGED
@@ -3,32 +3,32 @@
3
3
  MotionModel -- Simple Model, Validation, and Input Mixins for RubyMotion
4
4
  ================
5
5
 
6
- MotionModel is for cases where Core Data is too heavy to lift but you are
7
- still intending to work with your data.
6
+ MotionModel is a DSL for cases where Core Data is too heavy to lift but you are
7
+ still intending to work with your data, its types, and its relations.
8
8
 
9
- MotionModel is a bunch of "I don't ever want to have to write that code
10
- again if I can help it" things extracted into modules. The four modules
11
- are:
12
-
13
- - ext: Core Extensions that provide a few Rails-like niceties. Nothing
14
- new here, moving on...
15
-
16
- - model.rb: You can read about it in "What Model Can Do" but it's a
17
- mixin that provides you accessible attributes, row indexing,
18
- serialization for persistence, and some other niceties like row
19
- counting.
20
-
21
- - validatable.rb: Provides a basic validation framework for any
22
- arbitrary class. You can also create custom validations to suit
23
- your app's unique needs.
24
-
25
- - input_helpers: Hooking a collection up to a data form, populating
26
- the form, and retrieving the data afterwards can be a bunch of code.
27
- Not something I'd like to write more often that I have to.
9
+ File | Module | Description
10
+ ---------------------|---------------------------|------------------------------------
11
+ **ext.rb** | N/A | Core Extensions that provide a few Rails-like niceties. Nothing new here, moving on...
12
+ **model.rb** | MotionModel::Model | You can read about it in "What Model Can Do" but it's a mixin that provides you accessible attributes, row indexing, serialization for persistence, and some other niceties like row counting.
13
+ **validatable.rb** | MotionModel::Validatable | Provides a basic validation framework for any arbitrary class. You can also create custom validations to suit your app's unique needs.
14
+ **input_helpers** | MotionModel::InputHelpers | Helps hook a collection up to a data form, populate the form, and retrieve the data afterwards. Note: *MotionModel supports Formotion for input handling as well as these input helpers*.
15
+ **formotion.rb** | MotionModel::Formotion | Provides an interface between MotionModel and Formotion
28
16
 
29
17
  MotionModel is MIT licensed, which means you can pretty much do whatever
30
18
  you like with it. See the LICENSE file in this project.
31
19
 
20
+ * [Getting Going][]
21
+ * [What Model Can Do][]
22
+ * [Model Data Types][]
23
+ * [Validation Methods][]
24
+ * [Model Instances and Unique IDs][]
25
+ * [Using MotionModel][]
26
+ * [Notifications][]
27
+ * [Core Extensions][]
28
+ * [Formotion Support][]
29
+ * [Problems/Comments][]
30
+ * [pSubmissions/Patches][]
31
+
32
32
  Getting Going
33
33
  ================
34
34
 
@@ -125,6 +125,9 @@ a_task = Task.create(:name => 'joe-bob', :due_date => '2012-09-15') # due_da
125
125
  a_task.due_date = '2012-09-19' # due_date is cast to NSDate
126
126
  ```
127
127
 
128
+ Model Data Types
129
+ -----------
130
+
128
131
  Currently supported types are:
129
132
 
130
133
  * `:string`
@@ -138,9 +141,29 @@ Currently supported types are:
138
141
  You are really not encouraged to stuff big things in your models, which is why a blob type
139
142
  is not implemented. The smaller your data, the less overhead involved in saving/loading.
140
143
 
141
- What Validation Methods Exist
144
+ ### Special Columns
145
+
146
+ The two column names, `created_at` and `updated_at` will be adjusted automatically if they
147
+ are declared. They need to be of type `:date`. The `created_at` column will be set only when
148
+ the object is created (i.e., on first save). The `updated_at` column will change every time
149
+ the object is saved.
150
+
151
+ Validation Methods
142
152
  -----------------
143
153
 
154
+ To use validations in your model, declare your model as follows:
155
+
156
+ ```ruby
157
+ class MyValidatableModel
158
+ include MotionModel::Model
159
+ include MotionModel::Validatable
160
+
161
+ # All other model-y stuff here
162
+ end
163
+ ```
164
+
165
+ Here are some sample validations:
166
+
144
167
  validate :field_name, :presence => true
145
168
  validate :field_name, :length => 5..8 # specify a range
146
169
  validate :field_name, :email
@@ -166,11 +189,17 @@ In the above example, your new `validate_foo` method will get the arguments
166
189
  pretty much as you expect. The value of the
167
190
  last hash is passed intact via the `settings` argument.
168
191
 
192
+ You are responsible for adding an error message using:
193
+
194
+ add_message(field, "incorrect value foo #{the_foo} -- should be something else.")
195
+
196
+ You must return `true` from your validator if the value passes validation otherwise `false`.
197
+
169
198
  Model Instances and Unique IDs
170
199
  -----------------
171
200
 
172
201
  It is assumed that models can be created from an external source (JSON from a Web
173
- application or NSCoder from the device) or simply be a stand-alone data store.
202
+ application or `NSCoder` from the device) or simply be a stand-alone data store.
174
203
  To identify rows properly, the model tracks a special field called `:id`. If it's
175
204
  already present, it's left alone. If it's missing, then it is created for you.
176
205
  Each row id is guaranteed to be unique, so you can use this when communicating
@@ -300,7 +329,7 @@ The key to how the `destroy` variants work in how the relation is declared. You
300
329
  and `assignees` will *not be considered* when deleting `Task`s. However, by modifying the `has_many`,
301
330
 
302
331
  ```ruby
303
- has_many :assignees, :dependent => :destroy
332
+ has_many :assignees, :dependent => :destroy
304
333
  ```
305
334
 
306
335
  When you `destroy` an object, all of the objects related to it, and only those related
@@ -311,7 +340,7 @@ are left untouched.
311
340
  You can also specify:
312
341
 
313
342
  ```ruby
314
- has_many :assignees, :dependent => :delete
343
+ has_many :assignees, :dependent => :delete
315
344
  ```
316
345
 
317
346
  The difference here is that the cascade stops as the `assignees` are deleted so anything
@@ -322,34 +351,36 @@ Note: This syntax is modeled on the Rails `:dependent => :destroy` options in `A
322
351
  Notifications
323
352
  -------------
324
353
 
325
- Notifications are issued on object save, update, and delete. They work like this:
326
-
327
- ```ruby
328
- def viewDidAppear(animated)
329
- super
330
- # other stuff here to set up your view
331
-
332
- NSNotificationCenter.defaultCenter.addObserver(self, selector:'dataDidChange:', name:'MotionModelDataDidChangeNotification', object:nil)
333
- end
334
-
335
- def viewWillDisappear(animated)
336
- super
337
- NSNotificationCenter.defaultCenter.removeObserver self
338
- end
354
+ Notifications are issued on object save, update, and delete. They work like this:
339
355
 
340
- # ... more stuff ...
356
+ ```ruby
357
+ def viewDidAppear(animated)
358
+ super
359
+ # other stuff here to set up your view
341
360
 
342
- def dataDidChange(notification)
343
- # code to update or refresh your view based on the object passed back
344
- # and the userInfo. userInfo keys are:
345
- # action
346
- # 'add'
347
- # 'update'
348
- # 'delete'
349
- end
350
- ```
361
+ NSNotificationCenter.defaultCenter.addObserver(self, selector:'dataDidChange:',
362
+ name:'MotionModelDataDidChangeNotification',
363
+ object:nil)
364
+ end
365
+
366
+ def viewWillDisappear(animated)
367
+ super
368
+ NSNotificationCenter.defaultCenter.removeObserver self
369
+ end
370
+
371
+ # ... more stuff ...
372
+
373
+ def dataDidChange(notification)
374
+ # code to update or refresh your view based on the object passed back
375
+ # and the userInfo. userInfo keys are:
376
+ # action
377
+ # 'add'
378
+ # 'update'
379
+ # 'delete'
380
+ end
381
+ ```
351
382
 
352
- In your dataDidChange notification handler, you can respond to the `'MotionModelDataDidChangeNotification'` notification any way you like,
383
+ In your `dataDidChange` notification handler, you can respond to the `MotionModelDataDidChangeNotification` notification any way you like,
353
384
  but in the instance of a tableView, you might want to use the id of the object passed back to locate
354
385
  the correct row in the table and act upon it instead of doing a wholesale `reloadData`.
355
386
 
@@ -361,35 +392,35 @@ Notifications
361
392
  MotionModel does not currently send notification messages that differentiate by class, so if your
362
393
  UI presents `Task`s and you get a notification that an `Assignee` has changed:
363
394
 
364
- ```ruby
365
- class Task
366
- include MotionModel::Model
367
- has_many :assignees
368
- # etc
369
- end
370
-
371
- class Assignee
372
- include MotionModel::Model
373
- belongs_to :task
374
- # etc
375
- end
395
+ ```ruby
396
+ class Task
397
+ include MotionModel::Model
398
+ has_many :assignees
399
+ # etc
400
+ end
376
401
 
377
- # ...
378
-
379
- task = Task.create :name => 'Walk the dog' # Triggers notification with a task object
380
- task.assignees.create :name => 'Adam' # Triggers notification with an assignee object
381
-
382
- # ...
383
-
384
- # We set up observers for `MotionModelDataDidChangeNotification` someplace and:
385
- def dataDidChange(notification)
386
- if notification.object is_a?(Task)
387
- # Update our UI
388
- else
389
- # This notification is not for us because
390
- # We don't display anything other than tasks
391
- end
392
- ```
402
+ class Assignee
403
+ include MotionModel::Model
404
+ belongs_to :task
405
+ # etc
406
+ end
407
+
408
+ # ...
409
+
410
+ task = Task.create :name => 'Walk the dog' # Triggers notification with a task object
411
+ task.assignees.create :name => 'Adam' # Triggers notification with an assignee object
412
+
413
+ # ...
414
+
415
+ # We set up observers for `MotionModelDataDidChangeNotification` someplace and:
416
+ def dataDidChange(notification)
417
+ if notification.object is_a?(Task)
418
+ # Update our UI
419
+ else
420
+ # This notification is not for us because
421
+ # We don't display anything other than tasks
422
+ end
423
+ ```
393
424
 
394
425
  The above example implies you are only presenting, say, a list of tasks in the current
395
426
  view. If, however, you are presenting a list of tasks along with their assignees and
@@ -397,6 +428,7 @@ Notifications
397
428
  should recognize the change to assignee objects.
398
429
 
399
430
  Core Extensions
431
+ ----------------
400
432
 
401
433
  - String#humanize
402
434
  - String#titleize
@@ -450,7 +482,7 @@ Core Extensions
450
482
  Again, a reversing rule is required for both singularize and
451
483
  pluralize to work properly.
452
484
 
453
- Experimental: Formotion Support
485
+ Formotion Support
454
486
  ----------------------
455
487
 
456
488
  MotionModel now has support for the cool [Formotion gem](https://github.com/clayallsopp/formotion).
@@ -492,7 +524,15 @@ To initialize a form from a model in your controller:
492
524
 
493
525
  The magic is in: `MotionModel::Model#to_formotion(section_header)`.
494
526
 
495
- and on the flip side you do something like this in your submit handler:
527
+ The auto_date fields `created_at` and `updated_at` are not sent to
528
+ Formotion by default. If you want them sent to Formotion, set the
529
+ second argument to true. E.g.,
530
+
531
+ ```ruby
532
+ @form = Formotion::Form.new(@event.to_formotion('event details', true))
533
+ ```
534
+
535
+ On the flip side you do something like this in your Formotion submit handler:
496
536
 
497
537
  ```ruby
498
538
  @event.from_formotion!(data)
@@ -158,12 +158,38 @@ class Array
158
158
  def empty?
159
159
  self.length < 1
160
160
  end
161
+
162
+ # If any item in the array has the key == `key` true, otherwise false.
163
+ # Of good use when writing specs.
164
+ def has_hash_key?(key)
165
+ self.each do |entity|
166
+ return true if entity.has_key? key
167
+ end
168
+ return false
169
+ end
170
+
171
+ # If any item in the array has the value == `key` true, otherwise false
172
+ # Of good use when writing specs.
173
+ def has_hash_value?(key)
174
+ self.each do |entity|
175
+ entity.each_pair{|hash_key, value| return true if value == key}
176
+ end
177
+ return false
178
+ end
161
179
  end
162
180
 
181
+
182
+
163
183
  class Hash
164
184
  def empty?
165
185
  self.length < 1
166
186
  end
187
+
188
+ # Returns the contents of the hash, with the exception
189
+ # of the keys specified in the keys array.
190
+ def except(*keys)
191
+ self.dup.reject{|k, v| keys.include?(k)}
192
+ end
167
193
  end
168
194
 
169
195
  class Symbol
@@ -0,0 +1,55 @@
1
+ module MotionModel
2
+ module Formotion
3
+ FORMOTION_MAP = {
4
+ :string => :string,
5
+ :date => :date,
6
+ :int => :number,
7
+ :integer => :number,
8
+ :float => :number,
9
+ :double => :number,
10
+ :bool => :check,
11
+ :boolean => :check,
12
+ :text => :text
13
+ }
14
+
15
+ def returnable_columns
16
+ cols = columns.select do |column|
17
+ exposed = @expose_auto_date_fields ? true : ![:created_at, :updated_at].include?(column)
18
+ column != :id && # don't ship back id by default
19
+ !relation_column?(column) && # don't ship back relations -- formotion doesn't get them
20
+ exposed # don't expose auto_date fields unless specified
21
+ end
22
+ cols
23
+ end
24
+
25
+ def to_formotion(section_title = nil, expose_auto_date_fields = false)
26
+ @expose_auto_date_fields = expose_auto_date_fields
27
+ form = {
28
+ sections: [{}]
29
+ }
30
+
31
+ section = form[:sections].first
32
+ section[:title] ||= section_title
33
+ section[:rows] = []
34
+
35
+ returnable_columns.each do |column|
36
+ value = self.send(column)
37
+ value = value.to_f if type(column) == :date && value
38
+ h = {:key => column.to_sym,
39
+ :title => column.to_s.humanize,
40
+ :type => FORMOTION_MAP[type(column)],
41
+ :placeholder => column.to_s.humanize,
42
+ :value => value
43
+ }
44
+ options = column_named(column).options[:formotion]
45
+ h.merge!(options) if options
46
+ section[:rows].push h
47
+ end
48
+ form
49
+ end
50
+
51
+ def from_formotion!(data)
52
+ self.returnable_columns.each{|column| self.send("#{column}=", data[column])}
53
+ end
54
+ end
55
+ end
@@ -402,8 +402,10 @@ module MotionModel
402
402
 
403
403
  # Existing object implies update in place
404
404
  action = 'add'
405
+ set_auto_date_field 'created_at'
405
406
  if obj = collection.find{|o| o.id == @data[:id]}
406
407
  obj = self
408
+ set_auto_date_field 'updated_at'
407
409
  action = 'update'
408
410
  else
409
411
  collection << self
@@ -412,8 +414,12 @@ module MotionModel
412
414
  end
413
415
  end
414
416
 
415
- # Deletes the current object. The object can still be used.
417
+ # Set created_at and updated_at fields
418
+ def set_auto_date_field(field_name)
419
+ self.send("#{field_name}=", Time.now) if self.respond_to? field_name
420
+ end
416
421
 
422
+ # Deletes the current object. The object can still be used.
417
423
  def call_hook(hook_name, postfix)
418
424
  hook = "#{hook_name}_#{postfix}"
419
425
  self.send(hook, self) if respond_to? hook.to_sym
@@ -1,3 +1,3 @@
1
1
  module MotionModel
2
- VERSION = "0.3.2"
2
+ VERSION = "0.3.3"
3
3
  end
data/spec/date_spec.rb CHANGED
@@ -10,4 +10,32 @@ describe "time conversions" do
10
10
  strftime("%m-%d-%Y | %I:%M %p").
11
11
  should == "03-18-2012 | 07:00 PM"
12
12
  end
13
+
14
+ it "Sets created_at when an item is created" do
15
+ class Creatable
16
+ include MotionModel::Model
17
+ columns :name => :string,
18
+ :created_at => :date
19
+ end
20
+
21
+ c = Creatable.new(:name => 'test')
22
+ lambda{c.save}.should.change{c.created_at}
23
+ end
24
+
25
+ it "Sets updated_at when an item is created" do
26
+ class Updateable
27
+ include MotionModel::Model
28
+ columns :name => :string,
29
+ :created_at => :date,
30
+ :updated_at => :date
31
+ end
32
+
33
+ c = Updateable.create(:name => 'test')
34
+ c.name = 'test 1'
35
+ lambda{c.save}.should.not.change{c.created_at}
36
+ d = Updateable.create(:name => 'test')
37
+ d.name = 'test 2'
38
+ lambda{d.save}.should.change{d.updated_at}
39
+ end
40
+
13
41
  end
@@ -4,7 +4,18 @@ class ModelWithOptions
4
4
 
5
5
  columns :name => :string,
6
6
  :date => {:type => :date, :formotion => {:picker_type => :date_time}},
7
- :location => :string
7
+ :location => :string,
8
+ :created_at => :date,
9
+ :updated_at => :date
10
+
11
+ has_many :related_models
12
+ end
13
+
14
+ class RelatedModel
15
+ include MotionModel::Model
16
+
17
+ columns :name => :string
18
+ belongs_to :model_with_options
8
19
  end
9
20
 
10
21
  describe "formotion" do
@@ -30,4 +41,18 @@ describe "formotion" do
30
41
  @subject.date.strftime("%Y-%d-%d %H:%M:%S").should == '2013-03-03 12:00:00'
31
42
  @subject.location.should == "Q's Lab"
32
43
  end
44
+
45
+ it "does not include auto date fields in the hash by default" do
46
+ @subject.to_formotion[:sections].first[:rows].has_hash_key?(:created_at).should == false
47
+ @subject.to_formotion[:sections].first[:rows].has_hash_key?(:updated_at).should == false
48
+ end
49
+
50
+ it "can optionally include auto date fields in the hash" do
51
+ result = @subject.to_formotion(nil, true)[:sections].first[:rows].has_hash_value?(:created_at).should == true
52
+ result = @subject.to_formotion(nil, true)[:sections].first[:rows].has_hash_value?(:updated_at).should == true
53
+ end
54
+
55
+ it "does not include related columns in the collection" do
56
+ result = @subject.to_formotion[:sections].first[:rows].has_hash_value?(:related_models).should == false
57
+ end
33
58
  end
@@ -1,9 +1,3 @@
1
- class Hash
2
- def except(keys)
3
- self.dup.reject{|k, v| keys.include?(k)}
4
- end
5
- end
6
-
7
1
  class ValidatableTask
8
2
  include MotionModel::Model
9
3
  include MotionModel::Validatable
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: motion_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-03 00:00:00.000000000 Z
12
+ date: 2013-01-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bubble-wrap
@@ -45,6 +45,7 @@ files:
45
45
  - lib/motion_model/input_helpers.rb
46
46
  - lib/motion_model/model/column.rb
47
47
  - lib/motion_model/model/finder_query.rb
48
+ - lib/motion_model/model/formotion.rb
48
49
  - lib/motion_model/model/model.rb
49
50
  - lib/motion_model/model/model_casts.rb
50
51
  - lib/motion_model/model/persistence.rb