forminate 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +7 -2
- data/Gemfile +2 -0
- data/README.md +210 -4
- data/Rakefile +1 -1
- data/forminate.gemspec +21 -15
- data/lib/forminate/association_builder.rb +44 -0
- data/lib/forminate/association_definition.rb +22 -0
- data/lib/forminate/client_side_validations.rb +1 -1
- data/lib/forminate/version.rb +1 -1
- data/lib/forminate.rb +37 -77
- data/spec/forminate/association_builder_spec.rb +56 -0
- data/spec/forminate/association_definition_spec.rb +29 -0
- data/spec/{lib/forminate → forminate}/client_side_validations.rb +1 -1
- data/spec/{lib/forminate → forminate}/client_side_validations_spec.rb +10 -10
- data/spec/forminate_spec.rb +250 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/dummy_book.rb +0 -2
- data/spec/support/dummy_credit_card.rb +0 -2
- data/spec/support/dummy_user.rb +2 -14
- metadata +62 -28
- data/spec/lib/forminate_spec.rb +0 -179
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f117e333af88c5257424675b2ba3b235a3a7e514
|
4
|
+
data.tar.gz: 6a5483b6310fdc7b46341d53f3dcd1f8382474df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99a8d7568801c1732bbe22381569925e84da62555b8aa9edb1f2d3fa4fbb9bcd248c054db78414a868997d54dbff71cc2d5a9aa296585a9b63f7d238627bfeaa
|
7
|
+
data.tar.gz: 4eda18ed97e5a63be2fa985eafd15b403faf0640eefd5c2705dbc20c1a5c4d999f9e55ea1bf9d9de38182b650634c308e4c4369bb3f5dc5b6e319bb056d200ce
|
data/.travis.yml
CHANGED
@@ -2,5 +2,10 @@ language: ruby
|
|
2
2
|
rvm:
|
3
3
|
- 1.9.3
|
4
4
|
- 2.0.0
|
5
|
-
-
|
6
|
-
-
|
5
|
+
- 2.1.0
|
6
|
+
- jruby
|
7
|
+
- rbx
|
8
|
+
addons:
|
9
|
+
code_climate:
|
10
|
+
repo_token:
|
11
|
+
secure: AeJjEZpQ3k0FFhsjvSce9k8VBOjed5Qe0wd6PlRhO8Uuv73VZQTFScvHSV/p0BUNUYMAvvPFHD8ha2yaC0cbqKzdZVbgp3OLmv553CymlmEGdOZxhg5X7thnheAMF/SMxoqpB9X+MHDbQxnf6hYoa6ntWSpzh4bb7ruyR03sGYM=
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,19 @@
|
|
1
1
|
# Forminate
|
2
2
|
|
3
|
-
[![Build Status](https://
|
4
|
-
[![Code Climate](https://
|
3
|
+
[![Build Status](https://img.shields.io/travis/molawson/forminate.svg)](https://travis-ci.org/molawson/forminate)
|
4
|
+
[![Code Climate](https://img.shields.io/codeclimate/github/molawson/forminate.svg)](https://codeclimate.com/github/molawson/forminate)
|
5
|
+
[![Code Climate Coverage](https://img.shields.io/codeclimate/coverage/github/molawson/forminate.svg)](https://codeclimate.com/github/molawson/forminate)
|
5
6
|
|
6
|
-
|
7
|
+
Doing CRUD operations in Rails is pretty awesome. Just remember the first time you generated a Rails scaffold and almost immediately started creating and editing records in the database from a web form. I'd bet that hooked a lot of people. It certainly caught my attention.
|
8
|
+
|
9
|
+
Before too long, you need to create a page in a Rails app that has to update multiple models from a single form. Now, you feel the pain.
|
10
|
+
|
11
|
+
> Life is pain, Highness. Anyone who says differently is selling something.
|
12
|
+
_— Man in Black_
|
13
|
+
|
14
|
+
If you're at this point, let me introduce you to form objects. The general idea is that you create an object that represents the form you want to display in the view, and that form object handles aggregating and coordinating the various models that make up the form. For more information on the particulars of form objects and some example implementations in Rails, check out these great posts from [Code Climate](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/), [Thoughtbot](http://robots.thoughtbot.com/activemodel-form-objects), and [Pivotal Labs](http://pivotallabs.com/form-backing-objects-for-fun-and-profit/).
|
15
|
+
|
16
|
+
_Forminate gives you a handy way to create form objects that inherit behavior from the models you need and have just enough of the behavior you'd expect from an ActiveRecord or ActiveAttr model to make working with them feel very familiar._
|
7
17
|
|
8
18
|
## Installation
|
9
19
|
|
@@ -21,7 +31,203 @@ Or install it yourself as:
|
|
21
31
|
|
22
32
|
## Usage
|
23
33
|
|
24
|
-
|
34
|
+
### Requirements
|
35
|
+
|
36
|
+
Currently, forminate only works with ActiveRecord and [ActiveAttr](https://github.com/cgriego/active_attr) models. I would love to extend it to support other models (and it may actually work with others), but only these two have been tested.
|
37
|
+
|
38
|
+
### Example
|
39
|
+
|
40
|
+
To see how this works, lets take the classic example of a single page checkout process. We want to be able to have the user sign up and/or purchase a membership from a single form. We already have the following models in our system, all of which are needed on the checkout page.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class Membership < ActiveRecord::Base
|
44
|
+
# database columns: name, price
|
45
|
+
validates_presence_of :name
|
46
|
+
end
|
47
|
+
|
48
|
+
class User < ActiveRecord::Base
|
49
|
+
# database columns: first_name, last_name, email
|
50
|
+
validates_presence_of :email
|
51
|
+
attr_accessor :temporary_note
|
52
|
+
end
|
53
|
+
|
54
|
+
class CreditCard
|
55
|
+
include ActiveAttr::Model
|
56
|
+
|
57
|
+
attribute :number
|
58
|
+
attribute :expiration
|
59
|
+
attribute :cvv
|
60
|
+
|
61
|
+
validates_presence_of :number, :expiration, :cvv
|
62
|
+
validates_length_of :number, in: 12..19
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
To better model what's actually happening on the checkout page, we create a Cart form object that includes a user, membership, and credit_card.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class Cart
|
70
|
+
include Forminate
|
71
|
+
|
72
|
+
attribute :total
|
73
|
+
attribute :tax
|
74
|
+
|
75
|
+
attributes_for :user
|
76
|
+
attributes_for :membership, validate: false
|
77
|
+
attributes_for :credit_card, validate: :require_credit_card?
|
78
|
+
|
79
|
+
validates_numericality_of :total
|
80
|
+
|
81
|
+
def require_credit_card?
|
82
|
+
membership.price && membership.price.to_f > 0.0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
This small class gives us a lot of nice features.
|
88
|
+
|
89
|
+
### Attributes
|
90
|
+
|
91
|
+
The heart and soul of forminate is the `.attributes_for` method. Calling that method does a couple of things.
|
92
|
+
|
93
|
+
First, it sets up an association to an instance of the desired object, using the naming conventions you're used to in Rails, and exposes that object with reader and writer methods.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
cart = Cart.new
|
97
|
+
cart.credit_card # => #<CreditCard number: nil, expiration: nil, cvv: nil>
|
98
|
+
|
99
|
+
payment_card = CreditCard.new(number: 4242424242424242, expiration: 0115, cvv: 123)
|
100
|
+
cart.credit_card = payment_card
|
101
|
+
cart.credit_card # => #<CreditCard number: 4242424242424242, expiration: 0115, cvv: 123>
|
102
|
+
```
|
103
|
+
|
104
|
+
It also sets up reader and writer methods for all of the associated object's attributes To prevent method name conflicts, it prepends the underscore version of the model name.
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
cart = Cart.new
|
108
|
+
cart.credit_card_number # => nil
|
109
|
+
cart.credit_card_number = 4242424242424242
|
110
|
+
cart.credit_card_number # => 4242424242424242
|
111
|
+
```
|
112
|
+
|
113
|
+
Using these new attribute names, you can initialize your form object with a hash of attributes, just like ActiveRecord models.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
cart = Cart.new(credit_card_number: 4242424242424242, credit_card_expiration: 0115, credit_card_cvv: 123)
|
117
|
+
cart.credit_card_number # => 4242424242424242
|
118
|
+
cart.credit_card # => #<CreditCard number: 4242424242424242, expiration: 0115, cvv: 123>
|
119
|
+
```
|
120
|
+
|
121
|
+
#### Supported methods
|
122
|
+
|
123
|
+
Forminate explicitly sets up reader and writer methods for accessing methods related to database columns for ActiveRecord models or attributes for ActiveAttr models.
|
124
|
+
|
125
|
+
Additionally, you can call any method on an associated object via the form object by prepending the object's name, just like you do with other attributes. For example, the `User` class above has defined an `attr_accessor` for `temporary_note`.
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
cart = Cart.new
|
129
|
+
cart.user_temporary_note # => nil
|
130
|
+
cart.user_temporary_note = "I won't be here long"
|
131
|
+
cart.user_temporary_note # => "I won't be here long"
|
132
|
+
```
|
133
|
+
|
134
|
+
### Rails Forms
|
135
|
+
|
136
|
+
Now that we've got all these handy methods defined, we can get back to building those Rails forms we all know and love.
|
137
|
+
|
138
|
+
In your controller, you can create an instance variable for your form object like you would do with a normal model.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
class CartController < ApplicationController
|
142
|
+
def new
|
143
|
+
@cart = Cart.new
|
144
|
+
end
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
Then, you can setup your form view just like you'd expect.
|
149
|
+
|
150
|
+
```erb
|
151
|
+
<%= form_for @cart, url: cart_path, method: :post do |f| %>
|
152
|
+
<div class="field">
|
153
|
+
<%= f.label :user_email %>
|
154
|
+
<%= f.text_field :user_email %>
|
155
|
+
</div>
|
156
|
+
<div class="field">
|
157
|
+
<%= f.label :user_first_name %>
|
158
|
+
<%= f.text_field :user_first_name %>
|
159
|
+
</div>
|
160
|
+
<div class="field">
|
161
|
+
<%= f.label :user_last_name %>
|
162
|
+
<%= f.text_field :user_last_name %>
|
163
|
+
</div>
|
164
|
+
<div class="field">
|
165
|
+
<%= f.label :credit_card_number %>
|
166
|
+
<%= f.text_field :credit_card_number %>
|
167
|
+
</div>
|
168
|
+
<div class="field">
|
169
|
+
<%= f.label :credit_card_cvv %>
|
170
|
+
<%= f.text_field :credit_card_cvv %>
|
171
|
+
</div>
|
172
|
+
<%# etc., etc. %>
|
173
|
+
<% end %>
|
174
|
+
```
|
175
|
+
|
176
|
+
### Persistence
|
177
|
+
|
178
|
+
Forminate coordinates all the persistence for you. It includes a `#save` method that persists all the associated objects that also respond to `#save`. As long as ActiveRecord is available, forminate will wrap it's save in a single transaction, so if any of the associated models fails to save, it will roll everything back.
|
179
|
+
|
180
|
+
With this behavior, you can write your controller create actions just like you always have.
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
class CartController < ApplicationController
|
184
|
+
def new
|
185
|
+
@cart = Cart.new
|
186
|
+
end
|
187
|
+
|
188
|
+
def create
|
189
|
+
@cart = Cart.new(params[:cart])
|
190
|
+
if @cart.save
|
191
|
+
flash[:notice] = 'All good.'
|
192
|
+
redirect_to root_url
|
193
|
+
else
|
194
|
+
flash[:alert] = 'Something went terribly wrong.'
|
195
|
+
render :new
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
Forminate also exposes a `#before_save` hook method that can be used in your form object if you need to do any extra work just before the models are saved.
|
202
|
+
|
203
|
+
### Validations
|
204
|
+
|
205
|
+
By default, a forminate object will "inherit" all it's associated objects validations. Before saving it's associated objects, forminate will make sure that they're all valid. If not, it will return `false` and the form object will include an ActiveRecord-like errors object.
|
206
|
+
|
207
|
+
When calling `.attributes_for` to setup an associated object, you can pass a hash of options, which can include a `:validate` key. The value of the `:validate` key can be either, `true`, `false`, or a symbol that matches the name of a method that should be called to determine whether or not the association's validation should be checked (This is very similar to the `:if` option for the `.validates` methods in Rails).
|
208
|
+
|
209
|
+
From our example:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
class Cart
|
213
|
+
include Forminate
|
214
|
+
|
215
|
+
attribute :total
|
216
|
+
attribute :tax
|
217
|
+
|
218
|
+
attributes_for :user
|
219
|
+
attributes_for :membership, validate: false
|
220
|
+
attributes_for :credit_card, validate: :require_credit_card?
|
221
|
+
|
222
|
+
validates_numericality_of :total
|
223
|
+
|
224
|
+
def require_credit_card?
|
225
|
+
membership.price && membership.price.to_f > 0.0
|
226
|
+
end
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
In this case, if the membership that's being purchased is "free", we'll skip the credit card validations, and we won't bother with the membership validations at all.
|
25
231
|
|
26
232
|
## Contributing
|
27
233
|
|
data/Rakefile
CHANGED
data/forminate.gemspec
CHANGED
@@ -4,25 +4,31 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'forminate/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'forminate'
|
8
8
|
spec.version = Forminate::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.description =
|
12
|
-
spec.summary =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
9
|
+
spec.authors = ['Mo Lawson']
|
10
|
+
spec.email = ['mo@molawson.com']
|
11
|
+
spec.description = 'Form objects for Rails applications'
|
12
|
+
spec.summary = 'Create form objects from multiple Active Record and/or ActiveAttr models.'
|
13
|
+
spec.homepage = 'https://github.com/molawson/forminate'
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
|
-
spec.files = `git ls-files`.split(
|
16
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = [
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.add_dependency
|
22
|
-
spec.add_dependency
|
23
|
-
spec.add_dependency
|
21
|
+
spec.add_dependency 'active_attr', '~> 0.8'
|
22
|
+
spec.add_dependency 'activesupport', '>= 3.0.2', '< 4.1'
|
23
|
+
spec.add_dependency 'client_side_validations', '~> 3.2'
|
24
24
|
|
25
|
-
spec.add_development_dependency
|
26
|
-
spec.add_development_dependency
|
27
|
-
spec.add_development_dependency
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 2.11'
|
28
|
+
spec.add_development_dependency 'activerecord', '~> 3.2'
|
29
|
+
if defined?(JRUBY_VERSION)
|
30
|
+
spec.add_development_dependency 'activerecord-jdbcsqlite3-adapter'
|
31
|
+
else
|
32
|
+
spec.add_development_dependency 'sqlite3'
|
33
|
+
end
|
28
34
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Forminate
|
2
|
+
class AssociationBuilder
|
3
|
+
def initialize(name, attrs)
|
4
|
+
@name = name
|
5
|
+
@attrs = attrs
|
6
|
+
end
|
7
|
+
|
8
|
+
def build
|
9
|
+
if primary_key
|
10
|
+
object = klass.find(primary_key)
|
11
|
+
object.assign_attributes(association_attributes)
|
12
|
+
object
|
13
|
+
else
|
14
|
+
klass.new(association_attributes)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :name, :attrs
|
21
|
+
|
22
|
+
def klass
|
23
|
+
name.to_s.classify.constantize
|
24
|
+
end
|
25
|
+
|
26
|
+
def prefix
|
27
|
+
"#{name}_"
|
28
|
+
end
|
29
|
+
|
30
|
+
def primary_key
|
31
|
+
return unless klass.respond_to?(:primary_key)
|
32
|
+
|
33
|
+
attrs["#{name}_#{klass.primary_key}".to_sym]
|
34
|
+
end
|
35
|
+
|
36
|
+
def association_attributes
|
37
|
+
relevant_attributes = attrs.select { |k, _| k =~ /^#{prefix}/ }
|
38
|
+
relevant_attributes.each_with_object({}) do |(name, definition), hash|
|
39
|
+
new_key = name.to_s.sub(prefix, '').to_sym
|
40
|
+
hash[new_key] = definition
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Forminate
|
2
|
+
class AssociationDefinition
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name, options = {})
|
6
|
+
@name = name
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def attributes
|
11
|
+
name.to_s.classify.constantize.attribute_names
|
12
|
+
end
|
13
|
+
|
14
|
+
def validation_condition
|
15
|
+
options.fetch(:validate) { true }
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :options
|
21
|
+
end
|
22
|
+
end
|
@@ -71,7 +71,7 @@ module Forminate
|
|
71
71
|
# validation hash.
|
72
72
|
def association_validators
|
73
73
|
associations.reduce({}) do |assoc_validators, (name, object)|
|
74
|
-
if
|
74
|
+
if validate_assoc?(name) && object.respond_to?(:_validators)
|
75
75
|
object._validators.each do |attr, validators|
|
76
76
|
new_validators = validators.reduce([]) do |new_validators, validator|
|
77
77
|
new_validator = validator.dup
|
data/lib/forminate/version.rb
CHANGED
data/lib/forminate.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
require
|
1
|
+
require 'forminate/version'
|
2
2
|
|
3
3
|
require 'active_support/concern'
|
4
4
|
require 'active_attr'
|
5
5
|
|
6
|
+
require 'forminate/association_definition'
|
7
|
+
require 'forminate/association_builder'
|
8
|
+
|
6
9
|
module Forminate
|
7
10
|
extend ActiveSupport::Concern
|
8
11
|
include ActiveAttr::Model
|
@@ -10,10 +13,10 @@ module Forminate
|
|
10
13
|
included do
|
11
14
|
validate do
|
12
15
|
associations.each do |name, object|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
next unless validate_assoc?(name) && object.respond_to?(:invalid?) && object.invalid?
|
17
|
+
|
18
|
+
object.errors.each do |field, messages|
|
19
|
+
errors["#{name}_#{field}".to_sym] = messages
|
17
20
|
end
|
18
21
|
end
|
19
22
|
end
|
@@ -21,8 +24,7 @@ module Forminate
|
|
21
24
|
|
22
25
|
module ClassMethods
|
23
26
|
def attributes_for(name, options = {})
|
24
|
-
|
25
|
-
define_association name, options
|
27
|
+
define_association(AssociationDefinition.new(name, options))
|
26
28
|
end
|
27
29
|
|
28
30
|
def association_names
|
@@ -35,42 +37,33 @@ module Forminate
|
|
35
37
|
|
36
38
|
private
|
37
39
|
|
38
|
-
def
|
39
|
-
attributes
|
40
|
-
|
40
|
+
def define_association(assoc)
|
41
|
+
assoc.attributes.each { |attr| define_attribute(attr, assoc.name) }
|
42
|
+
association_names << assoc.name
|
43
|
+
association_validations[assoc.name] = assoc.validation_condition
|
44
|
+
send(:attr_accessor, assoc.name)
|
41
45
|
end
|
42
46
|
|
43
|
-
def define_attribute(attr,
|
44
|
-
ActiveAttr::AttributeDefinition.new("#{
|
47
|
+
def define_attribute(attr, assoc_name)
|
48
|
+
ActiveAttr::AttributeDefinition.new("#{assoc_name}_#{attr}").tap do |attribute_definition|
|
45
49
|
attribute_name = attribute_definition.name.to_s
|
46
50
|
attributes[attribute_name] = attribute_definition
|
47
51
|
end
|
48
|
-
define_attribute_reader(attr,
|
49
|
-
define_attribute_writer(attr,
|
52
|
+
define_attribute_reader(attr, assoc_name)
|
53
|
+
define_attribute_writer(attr, assoc_name)
|
50
54
|
end
|
51
55
|
|
52
|
-
def define_attribute_reader(attr,
|
53
|
-
define_method("#{
|
54
|
-
send(
|
56
|
+
def define_attribute_reader(attr, assoc_name)
|
57
|
+
define_method("#{assoc_name}_#{attr}") do
|
58
|
+
send(assoc_name.to_sym).send(attr.to_sym)
|
55
59
|
end
|
56
60
|
end
|
57
61
|
|
58
|
-
def define_attribute_writer(attr,
|
59
|
-
define_method("#{
|
60
|
-
send(
|
62
|
+
def define_attribute_writer(attr, assoc_name)
|
63
|
+
define_method("#{assoc_name}_#{attr}=") do |value|
|
64
|
+
send(assoc_name.to_sym).send("#{attr}=".to_sym, value)
|
61
65
|
end
|
62
66
|
end
|
63
|
-
|
64
|
-
def define_association(name, options = {})
|
65
|
-
association_names << name
|
66
|
-
should_validate = if options.has_key?(:validate)
|
67
|
-
options[:validate]
|
68
|
-
else
|
69
|
-
true
|
70
|
-
end
|
71
|
-
association_validations[name] = should_validate
|
72
|
-
send(:attr_accessor, name)
|
73
|
-
end
|
74
67
|
end
|
75
68
|
|
76
69
|
def initialize(attributes = {})
|
@@ -78,10 +71,6 @@ module Forminate
|
|
78
71
|
super
|
79
72
|
end
|
80
73
|
|
81
|
-
def persisted?
|
82
|
-
false
|
83
|
-
end
|
84
|
-
|
85
74
|
def association_names
|
86
75
|
self.class.association_names
|
87
76
|
end
|
@@ -128,69 +117,40 @@ module Forminate
|
|
128
117
|
|
129
118
|
def build_associations(attributes)
|
130
119
|
association_names.each do |association_name|
|
131
|
-
association =
|
120
|
+
association = AssociationBuilder.new(association_name, attributes).build
|
132
121
|
instance_variable_set("@#{association_name}".to_sym, association)
|
133
122
|
end
|
134
123
|
end
|
135
124
|
|
136
|
-
def build_association(name, attributes)
|
137
|
-
association_attributes = attributes_for_association(name, attributes)
|
138
|
-
klass = name.to_s.classify.constantize
|
139
|
-
|
140
|
-
if klass.respond_to? :primary_key
|
141
|
-
primary_key = attributes["#{name}_#{klass.primary_key}".to_sym]
|
142
|
-
end
|
143
|
-
|
144
|
-
if primary_key
|
145
|
-
object = klass.find primary_key
|
146
|
-
object.assign_attributes association_attributes
|
147
|
-
object
|
148
|
-
else
|
149
|
-
klass.new association_attributes
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
def attributes_for_association(association_name, attributes)
|
154
|
-
prefix = "#{association_name}_"
|
155
|
-
relevant_attributes = attributes.select { |k, v| k =~ /#{prefix}/ }
|
156
|
-
relevant_attributes.reduce({}) do |hash, (name, definition)|
|
157
|
-
new_key = name.to_s.sub(prefix, '').to_sym
|
158
|
-
hash[new_key] = definition
|
159
|
-
hash
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
125
|
def persist_associations
|
164
|
-
associations.each
|
165
|
-
object.save if object.respond_to? :save
|
166
|
-
end
|
126
|
+
associations.each { |_, object| object.save if object.respond_to?(:save) }
|
167
127
|
end
|
168
128
|
|
169
|
-
def
|
170
|
-
|
171
|
-
send(method_name)
|
129
|
+
def validate_assoc?(name)
|
130
|
+
send(assoc_validation_filter_method(name))
|
172
131
|
end
|
173
132
|
|
174
133
|
def assoc_validation_filter_method(name)
|
175
134
|
filter = self.class.association_validations.fetch(name, true)
|
176
|
-
method_name = "should_validate_#{name}?".to_sym
|
177
135
|
case filter
|
178
136
|
when Symbol
|
179
137
|
filter
|
180
138
|
when TrueClass, FalseClass
|
139
|
+
method_name = "validate_#{name}?".to_sym
|
181
140
|
self.class.send(:define_method, method_name) { filter }
|
182
141
|
method_name
|
183
142
|
else
|
184
|
-
|
143
|
+
fail NotImplementedError, 'The attributes_for :validate option can only take a symbol, true, or false'
|
185
144
|
end
|
186
145
|
end
|
187
146
|
|
188
147
|
def association_for_method(name)
|
189
|
-
assoc_name = association_names.find { |an| name.match
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
148
|
+
assoc_name = association_names.find { |an| name.match(/^#{an}_/) }
|
149
|
+
|
150
|
+
return unless assoc_name
|
151
|
+
|
152
|
+
assoc_method_name = name.to_s.sub("#{assoc_name}_", '').to_sym
|
153
|
+
assoc = send(assoc_name)
|
154
|
+
return assoc, assoc_method_name
|
195
155
|
end
|
196
156
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Forminate::AssociationBuilder do
|
4
|
+
let(:name) { 'dummy_book' }
|
5
|
+
let(:attrs) do
|
6
|
+
{
|
7
|
+
dummy_user_first_name: 'Mo',
|
8
|
+
dummy_book_title: 'The Hobbit'
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
subject { Forminate::AssociationBuilder.new(name, attrs) }
|
13
|
+
|
14
|
+
describe '#build' do
|
15
|
+
context 'non-ActiveRecord model' do
|
16
|
+
it 'populates the association with the given attributes' do
|
17
|
+
expect(subject.build.title).to eq('The Hobbit')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'ActiveRecord model' do
|
22
|
+
let(:name) { 'dummy_user' }
|
23
|
+
|
24
|
+
context 'primary key not present in attributes' do
|
25
|
+
it 'populates the association with the given attributes' do
|
26
|
+
expect(subject.build.first_name).to eq('Mo')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'primary key present in attributes' do
|
31
|
+
let(:user) do
|
32
|
+
DummyUser.create(
|
33
|
+
first_name: 'Mo',
|
34
|
+
last_name: 'Lawson',
|
35
|
+
email: 'mo@example.com'
|
36
|
+
)
|
37
|
+
end
|
38
|
+
let(:attrs) do
|
39
|
+
{
|
40
|
+
dummy_user_id: user.id,
|
41
|
+
dummy_user_first_name: 'Matthew',
|
42
|
+
dummy_book_title: 'The Hobbit'
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'populates the association from the database' do
|
47
|
+
expect(subject.build.last_name).to eq('Lawson')
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'overrides database values with passed in values' do
|
51
|
+
expect(subject.build.first_name).to eq('Matthew')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Forminate::AssociationDefinition do
|
4
|
+
let(:options) { {} }
|
5
|
+
|
6
|
+
subject { Forminate::AssociationDefinition.new(DummyUser, options) }
|
7
|
+
|
8
|
+
describe '#attributes' do
|
9
|
+
it 'returns a list of attributes from the appropriate class' do
|
10
|
+
expect(subject.attributes).to eq(%w(id first_name last_name email))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#validation_condition' do
|
15
|
+
context 'options hash does not contain :validate key' do
|
16
|
+
it 'returns true' do
|
17
|
+
expect(subject.validation_condition).to be_true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'options hash contains :validate key' do
|
22
|
+
let(:options) { { validate: :stars_aligned? } }
|
23
|
+
|
24
|
+
it 'returns the value' do
|
25
|
+
expect(subject.validation_condition).to eq(:stars_aligned?)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|