trans_forms 0.1.0
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.
- checksums.yaml +15 -0
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +88 -0
- data/Rakefile +27 -0
- data/lib/generators/rails/setup_generator.rb +14 -0
- data/lib/generators/rails/templates/application_trans_form.rb +13 -0
- data/lib/generators/rails/templates/trans_form.rb +31 -0
- data/lib/generators/rails/trans_form_generator.rb +36 -0
- data/lib/generators/rspec/setup_generator.rb +9 -0
- data/lib/generators/rspec/templates/application_trans_form_spec.rb +4 -0
- data/lib/generators/rspec/templates/trans_form_spec.rb +4 -0
- data/lib/generators/rspec/trans_form_generator.rb +9 -0
- data/lib/trans_forms.rb +9 -0
- data/lib/trans_forms/callbacks.rb +30 -0
- data/lib/trans_forms/errors.rb +13 -0
- data/lib/trans_forms/form_base.rb +47 -0
- data/lib/trans_forms/form_errors.rb +38 -0
- data/lib/trans_forms/main_model.rb +115 -0
- data/lib/trans_forms/main_model/proxy.rb +38 -0
- data/lib/trans_forms/nested_forms.rb +69 -0
- data/lib/trans_forms/version.rb +3 -0
- data/spec/generators/trans_form/trans_form_generator_spec.rb +8 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/support/models.rb +12 -0
- data/spec/support/schema.rb +23 -0
- data/spec/support/trans_forms/callback_forms.rb +22 -0
- data/spec/support/trans_forms/main_model_forms.rb +27 -0
- data/spec/support/trans_forms/multiple_records_forms.rb +31 -0
- data/spec/support/trans_forms/nested_forms.rb +26 -0
- data/spec/support/trans_forms/simple_forms.rb +13 -0
- data/spec/support/trans_forms/user_creator_1.rb +13 -0
- data/spec/trans_forms/callbacks_spec.rb +27 -0
- data/spec/trans_forms/form_base_spec.rb +70 -0
- data/spec/trans_forms/form_errors_spec.rb +14 -0
- data/spec/trans_forms/main_model/proxy_spec.rb +15 -0
- data/spec/trans_forms/main_model_spec.rb +49 -0
- data/spec/trans_forms/nested_forms_spec.rb +53 -0
- data/trans_forms.gemspec +29 -0
- metadata +214 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
module TransForms
|
2
|
+
module MainModel
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
# This method will extend the BaseForm functionality with the
|
8
|
+
# TransForms::MainModel::Active module.
|
9
|
+
def set_main_model(model, options = {})
|
10
|
+
include TransForms::MainModel::Active
|
11
|
+
|
12
|
+
# Stores the main_model record in a class_attribute
|
13
|
+
class_attribute :main_model
|
14
|
+
self.main_model = model
|
15
|
+
|
16
|
+
# Defines an instance accessor for the main_model
|
17
|
+
attr_accessor model
|
18
|
+
|
19
|
+
# Implements proxy module that overwrites model_name method
|
20
|
+
# to instead return an ActiveModel::Mame class for the
|
21
|
+
# main_model class
|
22
|
+
include TransForms::MainModel::Proxy if options[:proxy]
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
module Active
|
28
|
+
extend ActiveSupport::Concern
|
29
|
+
|
30
|
+
included do
|
31
|
+
# Since the after_save_on_error is not part of this module, we make sure
|
32
|
+
# that the methods exist before calling it. It should not be necessary since
|
33
|
+
# the Callbacks module is included by default in the FormBase, but added
|
34
|
+
# anyway. If this module is included manually (like in one of the specs) then
|
35
|
+
# it prevents any error to occur.
|
36
|
+
after_save_on_error :assert_record_on_error if respond_to?(:after_save_on_error)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Called from FormError to collect error messages from all possible
|
40
|
+
# models involved in the form transation.
|
41
|
+
def main_instance
|
42
|
+
send main_model
|
43
|
+
end
|
44
|
+
|
45
|
+
# Combines the errors from the FormModel as well as the main model instances
|
46
|
+
def errors
|
47
|
+
@errors ||= FormErrors.new(self, super)
|
48
|
+
end
|
49
|
+
|
50
|
+
# In it's default implementation, this method will look at the class name
|
51
|
+
# and try to match it to an attribute defined in the form model. If a match
|
52
|
+
# is found, then it will assign the model to that attribute.
|
53
|
+
#
|
54
|
+
# This method is encouraged to overwrite when there is custom conditions
|
55
|
+
# for how to handle the assignments.
|
56
|
+
#
|
57
|
+
# For example:
|
58
|
+
#
|
59
|
+
# class UserUpdater < ApplicationTransForm
|
60
|
+
# attr_accessor :admin
|
61
|
+
# set_main_model :user
|
62
|
+
#
|
63
|
+
# def model=(model)
|
64
|
+
# if model.role == :admin
|
65
|
+
# self.admin = model
|
66
|
+
# else
|
67
|
+
# self.user = model
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# # ...
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
def model=(model)
|
75
|
+
element = to_element(model)
|
76
|
+
if respond_to?("#{element}=")
|
77
|
+
send("#{element}=", model)
|
78
|
+
else
|
79
|
+
raise TransForms::NotImplementedError
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
# This method is assigned to the after_save_on_error callback
|
86
|
+
#
|
87
|
+
# If an error was raised in a create! statement then the variable might not
|
88
|
+
# have been assigned and would prevent error messages to be displayed. This
|
89
|
+
# makes sure that the instance that raised the error will be assigned if it
|
90
|
+
# would have been the main model.
|
91
|
+
def assert_record_on_error(e)
|
92
|
+
if e.respond_to?(:record) && e.record.present?
|
93
|
+
element = to_element(e.record)
|
94
|
+
if main_model == element.to_sym && send(element).nil?
|
95
|
+
send("#{element}=", e.record)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# A method to retrieve the underscored version of a record's class name.
|
101
|
+
# The procedure is slightly different in Rails 3 and Rails 4, that's what
|
102
|
+
# this method takes into consideration.
|
103
|
+
def to_element(record)
|
104
|
+
if record.class.model_name.is_a?(ActiveModel::Name)
|
105
|
+
record.class.model_name.element
|
106
|
+
else
|
107
|
+
record.class.model_name.underscore
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
TransForms::FormBase.send(:include, TransForms::MainModel)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module TransForms
|
2
|
+
module MainModel
|
3
|
+
module Proxy
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
# A module that adds functionality to delegate certain
|
6
|
+
# methods to the main model. This is done to make forms
|
7
|
+
# and controllers handle the form model as if it were
|
8
|
+
# the main model itself
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Returns an ActiveModel::Name object for module. It can be
|
13
|
+
# used to retrieve all kinds of naming-related information
|
14
|
+
# (See ActiveModel::Name for more information).
|
15
|
+
#
|
16
|
+
# class PostForm < TransForms::FormBase
|
17
|
+
# set_main_model :post, proxy: true
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# PostForm.model_name # => Post
|
21
|
+
# PostForm.model_name.class # => ActiveModel::Name
|
22
|
+
# PostForm.model_name.singular # => "post"
|
23
|
+
# PostForm.model_name.plural # => "posts"
|
24
|
+
def model_name
|
25
|
+
@_model_name ||= begin
|
26
|
+
klass = respond_to?(:main_model) ? main_model.to_s.classify.constantize : self
|
27
|
+
|
28
|
+
namespace = klass.parents.detect do |n|
|
29
|
+
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
30
|
+
end
|
31
|
+
ActiveModel::Name.new(klass, namespace)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module TransForms
|
2
|
+
module NestedForms
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
# Call this method from any Form class that uses a Hash attribute to
|
8
|
+
# process nested form models. It will include a number of methods
|
9
|
+
# to more efficiently go through params and records.
|
10
|
+
def has_nested_forms
|
11
|
+
include TransForms::NestedForms::Active
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
module Active
|
17
|
+
extend ActiveSupport::Concern
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
# A method that can be used to iterate through a Hash value that is
|
22
|
+
# structured the way Rails form helper +fields_for+ will create.
|
23
|
+
#
|
24
|
+
# Example:
|
25
|
+
#
|
26
|
+
# # Given the following Hash:
|
27
|
+
# attr = { '0' => { name: 'John' }, '0' => { name: 'Peter' } }
|
28
|
+
#
|
29
|
+
# # It can be processed like this:
|
30
|
+
# each_nested_hash_for attr do |nested_hash|
|
31
|
+
# name = nested_hash['name']
|
32
|
+
# # Do something with the name, like create multiple Contact
|
33
|
+
# # records
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
def each_nested_hash_for(attr, &block)
|
37
|
+
if attr.is_a?(Hash)
|
38
|
+
attr.values.each do |v|
|
39
|
+
if v.is_a?(Hash)
|
40
|
+
block.call(v.stringify_keys)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# A method that can be used to make sure that there are at least
|
47
|
+
# one nested hash that has a present value. Useful for validating
|
48
|
+
# an attribute when there needs to be at least one association
|
49
|
+
# created together with a main record.
|
50
|
+
def any_nested_hash_for?(attr)
|
51
|
+
attr.is_a?(Hash) && attr.values.detect { |v|
|
52
|
+
v.is_a?(Hash) && v.values.detect(&:present?).present?
|
53
|
+
}.present?
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_from!(collection, identifier, find_by = :id)
|
57
|
+
if identifier.present?
|
58
|
+
collection.detect { |instance| instance.send(find_by) == identifier.to_i } || (raise NotFoundFromError)
|
59
|
+
else
|
60
|
+
raise NotFoundFromError
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
TransForms::FormBase.send(:include, TransForms::NestedForms)
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
require 'bundler/setup'
|
9
|
+
require 'trans_forms'
|
10
|
+
require 'rails/version'
|
11
|
+
require 'support/schema'
|
12
|
+
require 'support/models'
|
13
|
+
require 'database_cleaner'
|
14
|
+
|
15
|
+
Dir[File.expand_path('../support/trans_forms/*.rb', __FILE__)].each { |f| require f }
|
16
|
+
|
17
|
+
I18n.enforce_available_locales = false
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
21
|
+
config.run_all_when_everything_filtered = true
|
22
|
+
config.filter_run :focus
|
23
|
+
|
24
|
+
config.before(:each) do
|
25
|
+
DatabaseCleaner.strategy = :transaction
|
26
|
+
DatabaseCleaner.start
|
27
|
+
end
|
28
|
+
|
29
|
+
config.after(:each) do
|
30
|
+
DatabaseCleaner.clean
|
31
|
+
end
|
32
|
+
|
33
|
+
# Run specs in random order to surface order dependencies. If you find an
|
34
|
+
# order dependency and want to debug it, you can fix the order by providing
|
35
|
+
# the seed, which is printed after each run.
|
36
|
+
# --seed 1234
|
37
|
+
config.order = 'random'
|
38
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class User < ActiveRecord::Base
|
2
|
+
has_many :phone_numbers, dependent: :destroy
|
3
|
+
|
4
|
+
validates :name, presence: true, uniqueness: true
|
5
|
+
end
|
6
|
+
|
7
|
+
class PhoneNumber < ActiveRecord::Base
|
8
|
+
belongs_to :user
|
9
|
+
|
10
|
+
validates :user_id, presence: true
|
11
|
+
validates :number, presence: true, uniqueness: { scope: :user_id }
|
12
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
ActiveRecord::Base.establish_connection({
|
4
|
+
adapter: 'sqlite3',
|
5
|
+
database: ':memory:'
|
6
|
+
})
|
7
|
+
|
8
|
+
# prepare test data
|
9
|
+
class CreateTestSchema < ActiveRecord::Migration
|
10
|
+
def change
|
11
|
+
create_table "users", :force => true do |t|
|
12
|
+
t.string "name"
|
13
|
+
t.integer "age"
|
14
|
+
end
|
15
|
+
|
16
|
+
create_table "phone_numbers", :force => true do |t|
|
17
|
+
t.integer "user_id"
|
18
|
+
t.string "number"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
CreateTestSchema.migrate(:up)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class CallbackForm < TransForms::FormBase
|
2
|
+
attr_reader :user1, :user2, :i_was_called
|
3
|
+
|
4
|
+
attribute :name1, String
|
5
|
+
attribute :name2, String
|
6
|
+
|
7
|
+
validates :name1, presence: true
|
8
|
+
validates :name2, presence: true
|
9
|
+
|
10
|
+
after_save_on_error :call_me
|
11
|
+
|
12
|
+
transaction do
|
13
|
+
@user1 = User.create!(name: name1)
|
14
|
+
@user2 = User.create!(name: name2)
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
def call_me(error)
|
19
|
+
@i_was_called = true
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class MainModelModel
|
2
|
+
include TransForms::MainModel
|
3
|
+
end
|
4
|
+
|
5
|
+
class ProxyModel
|
6
|
+
class_attribute :main_model
|
7
|
+
self.main_model = :user
|
8
|
+
include TransForms::MainModel::Proxy
|
9
|
+
end
|
10
|
+
|
11
|
+
class UserUpdater < TransForms::FormBase
|
12
|
+
set_main_model :user
|
13
|
+
|
14
|
+
attribute :name, String
|
15
|
+
|
16
|
+
validates :name, presence: true
|
17
|
+
|
18
|
+
transaction do
|
19
|
+
user.attributes = { name: name }
|
20
|
+
user.save!
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class UserProxyModel < TransForms::FormBase
|
26
|
+
set_main_model :user, proxy: true
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class MultipleRecordsCreator < TransForms::FormBase
|
2
|
+
attr_reader :user1, :user2
|
3
|
+
|
4
|
+
attribute :name1, String
|
5
|
+
attribute :name2, String
|
6
|
+
|
7
|
+
validates :name1, presence: true
|
8
|
+
validates :name2, presence: true
|
9
|
+
|
10
|
+
transaction do
|
11
|
+
@user1 = User.create!(name: name1)
|
12
|
+
@user2 = User.create!(name: name2)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
class MultipleRecordsCreatorNoBang < TransForms::FormBase
|
18
|
+
attr_reader :user1, :user2
|
19
|
+
|
20
|
+
attribute :name1, String
|
21
|
+
attribute :name2, String
|
22
|
+
|
23
|
+
validates :name1, presence: true
|
24
|
+
validates :name2, presence: true
|
25
|
+
|
26
|
+
transaction do
|
27
|
+
@user1 = User.create(name: name1)
|
28
|
+
@user2 = User.create(name: name2)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class NestedFormsModel
|
2
|
+
include TransForms::NestedForms::Active
|
3
|
+
|
4
|
+
# Makes the methods public for easier Unit testing
|
5
|
+
public :each_nested_hash_for, :any_nested_hash_for?, :find_from!
|
6
|
+
end
|
7
|
+
|
8
|
+
class UserAndPoneNumbersCreator < TransForms::FormBase
|
9
|
+
has_nested_forms
|
10
|
+
|
11
|
+
attr_reader :user
|
12
|
+
|
13
|
+
attribute :name, String
|
14
|
+
attribute :phone_numbers_attributes, Hash
|
15
|
+
|
16
|
+
validates :name, presence: true
|
17
|
+
|
18
|
+
transaction do
|
19
|
+
@user = User.create!(name: name)
|
20
|
+
|
21
|
+
each_nested_hash_for phone_numbers_attributes do |attr|
|
22
|
+
@user.phone_numbers.create!(attr)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class NoErrorInTransactionForm < TransForms::FormBase
|
2
|
+
transaction do
|
3
|
+
# Bunch of actions
|
4
|
+
1 + 1; 'No errors'; [1,2,3].map(&:to_s)
|
5
|
+
false # Can return false, save will be success anyway
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class ErrorInTransactionForm < TransForms::FormBase
|
10
|
+
transaction do
|
11
|
+
raise ActiveRecord::ActiveRecordError
|
12
|
+
end
|
13
|
+
end
|