motion_model 0.3.2 → 0.3.3
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/CHANGELOG +8 -0
- data/README.md +120 -80
- data/lib/motion_model/ext.rb +26 -0
- data/lib/motion_model/model/formotion.rb +55 -0
- data/lib/motion_model/model/model.rb +7 -1
- data/lib/motion_model/version.rb +1 -1
- data/spec/date_spec.rb +28 -0
- data/spec/formotion_spec.rb +26 -1
- data/spec/validation_spec.rb +0 -6
- metadata +3 -2
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
356
|
+
```ruby
|
357
|
+
def viewDidAppear(animated)
|
358
|
+
super
|
359
|
+
# other stuff here to set up your view
|
341
360
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
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 `
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
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
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
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
|
-
|
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
|
-
|
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)
|
data/lib/motion_model/ext.rb
CHANGED
@@ -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
|
-
#
|
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
|
data/lib/motion_model/version.rb
CHANGED
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
|
data/spec/formotion_spec.rb
CHANGED
@@ -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
|
data/spec/validation_spec.rb
CHANGED
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.
|
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-
|
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
|