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 CHANGED
@@ -1,3 +1,5 @@
1
+ 2012-01-24: Added block-structured transactions.
2
+
1
3
  2012-01-14: Fixed problem where data returned from forms was of type NSString, which
2
4
  confused some monkey-patching code.
3
5
  Changed before_ hooks such that handlers returning false would terminate
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
- * [Notifications][]
27
- * [Core Extensions][]
28
- * [Formotion Support][]
29
- * [Problems/Comments][]
30
- * [pSubmissions/Patches][]
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
- * `text`
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 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
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 = 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
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| self.send("#{column}=", data[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
@@ -1,4 +1,4 @@
1
1
 
2
2
  module MotionModel
3
- VERSION = "0.3.5"
3
+ VERSION = "0.3.6"
4
4
  end
@@ -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 => '3-3-13', :location => "Q's Lab"})
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-%d-%d %H:%M:%S").should == '2013-03-03 12:00:00'
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.5
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-14 00:00:00.000000000 Z
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