motion_model 0.3.5 → 0.3.6
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +2 -0
- data/README.md +39 -12
- data/lib/motion_model/model/formotion.rb +47 -20
- data/lib/motion_model/model/transaction.rb +43 -0
- data/lib/motion_model/validatable.rb +1 -1
- data/lib/motion_model/version.rb +1 -1
- data/spec/formotion_spec.rb +18 -2
- data/spec/transaction_spec.rb +91 -0
- metadata +5 -2
data/CHANGELOG
CHANGED
data/README.md
CHANGED
@@ -13,21 +13,23 @@ File | Module | Description
|
|
13
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
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
15
|
**formotion.rb** | MotionModel::Formotion | Provides an interface between MotionModel and Formotion
|
16
|
+
**transaction.rb** | MotionModel::Model::Transactions | Provides transaction support for model modifications
|
16
17
|
|
17
18
|
MotionModel is MIT licensed, which means you can pretty much do whatever
|
18
19
|
you like with it. See the LICENSE file in this project.
|
19
20
|
|
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
|
-
* [
|
27
|
-
* [
|
28
|
-
* [
|
29
|
-
* [
|
30
|
-
* [
|
21
|
+
* [Getting Going](#getting-going)
|
22
|
+
* [What Model Can Do](#what-model-can-do)
|
23
|
+
* [Model Data Types](#model-data-types)
|
24
|
+
* [Validation Methods](#validation-methods)
|
25
|
+
* [Model Instances and Unique IDs](#model-instances-and-unique-ids)
|
26
|
+
* [Using MotionModel](#using-motionmodel)
|
27
|
+
* [Transactions and Undo/Cancel](#transactions-and-undocancel)
|
28
|
+
* [Notifications](#notifications)
|
29
|
+
* [Core Extensions](#core-extensions)
|
30
|
+
* [Formotion Support](#formotion-support)
|
31
|
+
* [Problems/Comments](#problemscomments)
|
32
|
+
* [Submissions/Patches](#submissionspatches)
|
31
33
|
|
32
34
|
Getting Going
|
33
35
|
================
|
@@ -131,7 +133,7 @@ Model Data Types
|
|
131
133
|
Currently supported types are:
|
132
134
|
|
133
135
|
* `:string`
|
134
|
-
*
|
136
|
+
* `:text`
|
135
137
|
* `:boolean`, `:bool`
|
136
138
|
* `:int`, `:integer`
|
137
139
|
* `:float`, `:double`
|
@@ -348,6 +350,31 @@ related to the assignees remains intact.
|
|
348
350
|
|
349
351
|
Note: This syntax is modeled on the Rails `:dependent => :destroy` options in `ActiveRecord`.
|
350
352
|
|
353
|
+
## Transactions and Undo/Cancel
|
354
|
+
|
355
|
+
MotionModel is not ActiveRecord. MotionModel is not a database-backed mapper. The bottom line is that when you change a field in a model, even if you don't save it, you are partying on the central object store. In part, this is because Ruby copies objects by reference, so when you do a find, you get a reference to the object *in the central object store*.
|
356
|
+
|
357
|
+
The upshot of this is that MotionModel can be wicked fast because it isn't moving much more than pointers around in memory when you do assignments. However, it can be surprising if you are used to a database-backed mapper.
|
358
|
+
|
359
|
+
You could easily build an app and never run across a problem with this, but in the case where you present a dialog with a cancel button, you will need a way to back out. Here's how:
|
360
|
+
|
361
|
+
```ruby
|
362
|
+
# in your form presentation view...
|
363
|
+
include MotionModel::Model::Transactions
|
364
|
+
|
365
|
+
person.transaction do
|
366
|
+
result = do_something_that_changes_person
|
367
|
+
person.rollback unless result
|
368
|
+
end
|
369
|
+
|
370
|
+
def do_something_that_changes_person
|
371
|
+
# stuff
|
372
|
+
return it_worked
|
373
|
+
end
|
374
|
+
```
|
375
|
+
|
376
|
+
You can have nested transactions and each has its own context so you don't wind up rolling back to the wrong state. However, everything that you wrap in a transaction must be wrapped in the `transaction` block. That means you need to have some outer calling method that can wrap a series of delegated changes. Explained differently, you can't start a transaction, have a delegate method handle a cancel button click and roll back the transaction from inside the delegate method. When the block is exited, the transaction context is removed.
|
377
|
+
|
351
378
|
Notifications
|
352
379
|
-------------
|
353
380
|
|
@@ -12,16 +12,43 @@ module MotionModel
|
|
12
12
|
:text => :text
|
13
13
|
}
|
14
14
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
15
|
+
def should_return(column) #nodoc
|
16
|
+
skippable = [:id]
|
17
|
+
skippable += [:created_at, :updated_at] unless @expose_auto_date_fields
|
18
|
+
!skippable.include?(column) && !relation_column?(column)
|
19
|
+
end
|
20
|
+
|
21
|
+
def returnable_columns #nodoc
|
22
|
+
columns.select{|column| should_return(column)}
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_hash_for(column, value)
|
26
|
+
{:key => column.to_sym,
|
27
|
+
:title => column.to_s.humanize,
|
28
|
+
:type => FORMOTION_MAP[type(column)],
|
29
|
+
:placeholder => column.to_s.humanize,
|
30
|
+
:value => value
|
31
|
+
}
|
23
32
|
end
|
24
33
|
|
34
|
+
def value_for(column) #nodoc
|
35
|
+
value = self.send(column)
|
36
|
+
value = value.to_f if type(column) == :date && !value.nil?
|
37
|
+
value
|
38
|
+
end
|
39
|
+
|
40
|
+
def combine_options(column, hash) #nodoc
|
41
|
+
options = column_named(column).options[:formotion]
|
42
|
+
options ? hash.merge(options) : hash
|
43
|
+
end
|
44
|
+
|
45
|
+
# <tt>to_formotion</tt> maps a MotionModel into a hash suitable for creating
|
46
|
+
# a Formotion form. By default, the auto date fields, <tt>created_at</tt>
|
47
|
+
# and <tt>updated_at</tt> are suppressed. If you want these shown in
|
48
|
+
# your Formotion form, set <tt>expose_auto_date_fields</tt> to <tt>true</tt>
|
49
|
+
#
|
50
|
+
# If you want a title for your Formotion form, set the <tt>section_title</tt>
|
51
|
+
# argument to a string that will become that title.
|
25
52
|
def to_formotion(section_title = nil, expose_auto_date_fields = false)
|
26
53
|
@expose_auto_date_fields = expose_auto_date_fields
|
27
54
|
form = {
|
@@ -33,23 +60,23 @@ module MotionModel
|
|
33
60
|
section[:rows] = []
|
34
61
|
|
35
62
|
returnable_columns.each do |column|
|
36
|
-
value =
|
37
|
-
|
38
|
-
|
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
|
63
|
+
value = value_for(column)
|
64
|
+
h = default_hash_for(column, value)
|
65
|
+
section[:rows].push(combine_options(column, h))
|
47
66
|
end
|
48
67
|
form
|
49
68
|
end
|
50
69
|
|
70
|
+
# <tt>from_formotion</tt> takes the information rendered from a Formotion
|
71
|
+
# form and stuffs it back into a MotionModel. This data is not saved until
|
72
|
+
# you say so, offering you the opportunity to validate your form data.
|
51
73
|
def from_formotion!(data)
|
52
|
-
self.returnable_columns.each{|column|
|
74
|
+
self.returnable_columns.each{|column|
|
75
|
+
if type(column) == :date && !data[column].nil?
|
76
|
+
data[column] = Time.at(data[column])
|
77
|
+
end
|
78
|
+
value = self.send("#{column}=", data[column])
|
79
|
+
}
|
53
80
|
end
|
54
81
|
end
|
55
82
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module MotionModel
|
2
|
+
module Model
|
3
|
+
module Transactions
|
4
|
+
def transaction(&block)
|
5
|
+
if block_given?
|
6
|
+
@savepoints = [] if @savepoints.nil?
|
7
|
+
@savepoints.push self.duplicate
|
8
|
+
yield
|
9
|
+
@savepoints.pop
|
10
|
+
else
|
11
|
+
raise ArgumentError.new("transaction must have a block")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def rollback
|
16
|
+
unless @savepoints.empty?
|
17
|
+
restore_attributes
|
18
|
+
else
|
19
|
+
NSLog "No savepoint, so rollback not performed."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def columns_without_relations
|
24
|
+
columns.select{|col| ![:has_many, :belongs_to].include?(type(col))}
|
25
|
+
end
|
26
|
+
|
27
|
+
def restore_attributes
|
28
|
+
savepoint = @savepoints.last
|
29
|
+
if savepoint.nil?
|
30
|
+
NSLog "No savepoint, so rollback not performed."
|
31
|
+
else
|
32
|
+
columns_without_relations.each do |col|
|
33
|
+
self.send("#{col}=", savepoint.send(col))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def duplicate
|
39
|
+
Marshal.load(Marshal.dump(self))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -110,7 +110,7 @@ module MotionModel
|
|
110
110
|
if value.is_a?(String) || value.nil?
|
111
111
|
result = value.nil? || ((value.length == 0) == setting)
|
112
112
|
additional_message = setting ? "non-empty" : "non-empty"
|
113
|
-
add_message(field, "incorrect value supplied for #{field.to_s} -- should be #{additional_message}.") if result
|
113
|
+
add_message(field, "incorrect value supplied for #{field.to_s.humanize} -- should be #{additional_message}.") if result
|
114
114
|
return !result
|
115
115
|
end
|
116
116
|
return false
|
data/lib/motion_model/version.rb
CHANGED
data/spec/formotion_spec.rb
CHANGED
@@ -18,6 +18,18 @@ class RelatedModel
|
|
18
18
|
belongs_to :model_with_options
|
19
19
|
end
|
20
20
|
|
21
|
+
def section(subject)
|
22
|
+
subject[:sections]
|
23
|
+
end
|
24
|
+
|
25
|
+
def rows(subject)
|
26
|
+
section(subject).first[:rows]
|
27
|
+
end
|
28
|
+
|
29
|
+
def first_row(subject)
|
30
|
+
rows(subject).first
|
31
|
+
end
|
32
|
+
|
21
33
|
describe "formotion" do
|
22
34
|
before do
|
23
35
|
@subject = ModelWithOptions.create(:name => 'get together', :date => '12-11-13 @ 9:00 PM', :location => 'my house')
|
@@ -35,10 +47,14 @@ describe "formotion" do
|
|
35
47
|
@subject.to_formotion('test section')[:sections].first[:rows].length.should == 3
|
36
48
|
end
|
37
49
|
|
50
|
+
it "value of name row is 'get together'" do
|
51
|
+
first_row(@subject.to_formotion)[:value].should == 'get together'
|
52
|
+
end
|
53
|
+
|
38
54
|
it "binds data from rendered form into model fields" do
|
39
|
-
@subject.from_formotion!({:name => '007 Reunion', :date =>
|
55
|
+
@subject.from_formotion!({:name => '007 Reunion', :date => 1358197323, :location => "Q's Lab"})
|
40
56
|
@subject.name.should == '007 Reunion'
|
41
|
-
@subject.date.strftime("%Y-%
|
57
|
+
@subject.date.strftime("%Y-%m-%d %H:%M").should == '2013-01-14 13:02'
|
42
58
|
@subject.location.should == "Q's Lab"
|
43
59
|
end
|
44
60
|
|
@@ -0,0 +1,91 @@
|
|
1
|
+
class TransactClass
|
2
|
+
include MotionModel::Model
|
3
|
+
include MotionModel::Model::Transactions
|
4
|
+
columns :name, :age
|
5
|
+
has_many :transaction_things
|
6
|
+
end
|
7
|
+
|
8
|
+
class TransactionThing
|
9
|
+
include MotionModel::Model
|
10
|
+
include MotionModel::Model::Transactions
|
11
|
+
columns :thingie_description
|
12
|
+
belongs_to :transact_class
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "transactions" do
|
16
|
+
before{TransactClass.destroy_all}
|
17
|
+
|
18
|
+
it "wraps a transaction but auto-commits" do
|
19
|
+
item = TransactClass.create(:name => 'joe', :age => 22)
|
20
|
+
item.transaction do
|
21
|
+
item.name = 'Bob'
|
22
|
+
end
|
23
|
+
item.name.should == 'Bob'
|
24
|
+
TransactClass.find(:name).eq('Bob').count.should == 1
|
25
|
+
end
|
26
|
+
|
27
|
+
it "wraps a transaction but can rollback to a savepoint" do
|
28
|
+
item = TransactClass.create(:name => 'joe', :age => 22)
|
29
|
+
item.transaction do
|
30
|
+
item.name = 'Bob'
|
31
|
+
item.rollback
|
32
|
+
end
|
33
|
+
item.name.should == 'joe'
|
34
|
+
TransactClass.find(:name).eq('joe').count.should == 1
|
35
|
+
TransactClass.find(:name).eq('Bob').count.should == 0
|
36
|
+
end
|
37
|
+
|
38
|
+
it "allows multiple savepoints -- inside one not exercised" do
|
39
|
+
item = TransactClass.create(:name => 'joe', :age => 22)
|
40
|
+
item.transaction do
|
41
|
+
item.transaction do
|
42
|
+
item.name = 'Bob'
|
43
|
+
end
|
44
|
+
item.rollback
|
45
|
+
item.name.should == 'joe'
|
46
|
+
TransactClass.find(:name).eq('joe').count.should == 1
|
47
|
+
TransactClass.find(:name).eq('Bob').count.should == 0
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "allows multiple savepoints -- inside one exercised" do
|
52
|
+
item = TransactClass.create(:name => 'joe', :age => 22)
|
53
|
+
item.transaction do
|
54
|
+
item.transaction do
|
55
|
+
item.name = 'Ralph'
|
56
|
+
item.rollback
|
57
|
+
end
|
58
|
+
item.name.should == 'joe'
|
59
|
+
TransactClass.find(:name).eq('joe').count.should == 1
|
60
|
+
TransactClass.find(:name).eq('Bob').count.should == 0
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "allows multiple savepoints -- set in outside context rollback in inside" do
|
65
|
+
item = TransactClass.create(:name => 'joe', :age => 22)
|
66
|
+
item.transaction do
|
67
|
+
item.name = 'Ralph'
|
68
|
+
item.transaction do
|
69
|
+
item.rollback
|
70
|
+
end
|
71
|
+
item.name.should == 'Ralph'
|
72
|
+
TransactClass.find(:name).eq('Ralph').count.should == 1
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "allows multiple savepoints -- multiple savepoints exercised" do
|
77
|
+
item = TransactClass.create(:name => 'joe', :age => 22)
|
78
|
+
item.transaction do
|
79
|
+
item.name = 'Ralph'
|
80
|
+
item.transaction do
|
81
|
+
item.name = 'Paul'
|
82
|
+
item.rollback
|
83
|
+
item.name.should == 'Ralph'
|
84
|
+
TransactClass.find(:name).eq('Ralph').count.should == 1
|
85
|
+
end
|
86
|
+
item.rollback
|
87
|
+
item.name.should == 'joe'
|
88
|
+
TransactClass.find(:name).eq('joe').count.should == 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
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.6
|
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-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bubble-wrap
|
@@ -49,6 +49,7 @@ files:
|
|
49
49
|
- lib/motion_model/model/model.rb
|
50
50
|
- lib/motion_model/model/model_casts.rb
|
51
51
|
- lib/motion_model/model/persistence.rb
|
52
|
+
- lib/motion_model/model/transaction.rb
|
52
53
|
- lib/motion_model/validatable.rb
|
53
54
|
- lib/motion_model/version.rb
|
54
55
|
- motion_model.gemspec
|
@@ -64,6 +65,7 @@ files:
|
|
64
65
|
- spec/notification_spec.rb
|
65
66
|
- spec/persistence_spec.rb
|
66
67
|
- spec/relation_spec.rb
|
68
|
+
- spec/transaction_spec.rb
|
67
69
|
- spec/validation_spec.rb
|
68
70
|
homepage: https://github.com/sxross/MotionModel
|
69
71
|
licenses: []
|
@@ -102,4 +104,5 @@ test_files:
|
|
102
104
|
- spec/notification_spec.rb
|
103
105
|
- spec/persistence_spec.rb
|
104
106
|
- spec/relation_spec.rb
|
107
|
+
- spec/transaction_spec.rb
|
105
108
|
- spec/validation_spec.rb
|