--------------------------------------------------------------------------------
/test/dummy/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/test/dummy/README.rdoc:
--------------------------------------------------------------------------------
1 | == README
2 |
3 | This README would normally document whatever steps are necessary to get the
4 | application up and running.
5 |
6 | Things you may want to cover:
7 |
8 | * Ruby version
9 |
10 | * System dependencies
11 |
12 | * Configuration
13 |
14 | * Database creation
15 |
16 | * Database initialization
17 |
18 | * How to run the test suite
19 |
20 | * Services (job queues, cache servers, search engines, etc.)
21 |
22 | * Deployment instructions
23 |
24 | * ...
25 |
26 |
27 | Please feel free to use a different markup language if you do not plan to run
28 | rake doc:app.
29 |
--------------------------------------------------------------------------------
/lib/action_form.rb:
--------------------------------------------------------------------------------
1 | module ActionForm
2 | autoload :Base, 'action_form/base'
3 | autoload :Form, 'action_form/form'
4 | autoload :FormCollection, 'action_form/form_collection'
5 | autoload :FormDefinition, 'action_form/form_definition'
6 | autoload :TooManyRecords, 'action_form/too_many_records'
7 | autoload :ViewHelpers, 'action_form/view_helpers'
8 | autoload :FormHelpers, 'action_form/form_helpers'
9 |
10 | class Engine < ::Rails::Engine
11 | initializer "action_form.initialize" do |app|
12 | ActiveSupport.on_load :action_view do
13 | include ActionForm::ViewHelpers
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/dummy/app/forms/project_form.rb:
--------------------------------------------------------------------------------
1 | class ProjectForm < ActionForm::Base
2 | attributes :name, :description, :owner_id
3 |
4 | association :tasks do
5 | attributes :name, :description, :done
6 |
7 | association :sub_tasks do
8 | attributes :name, :description, :done
9 | end
10 | end
11 |
12 | association :contributors, records: 2 do
13 | attributes :name, :description, :role
14 | end
15 |
16 | association :project_tags do
17 | attribute :tag_id
18 |
19 | association :tag do
20 | attribute :name
21 | end
22 | end
23 |
24 | association :owner do
25 | attributes :name, :description, :role
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/dummy/app/views/surveys/index.html.erb:
--------------------------------------------------------------------------------
1 |
32 | <% end %>
33 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | Bundler.require(*Rails.groups)
6 | require "action_form"
7 |
8 | module Dummy
9 | class Application < Rails::Application
10 | # Settings in config/environments/* take precedence over those specified here.
11 | # Application configuration should go into files in config/initializers
12 | # -- all .rb files in that directory are automatically loaded.
13 |
14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
16 | # config.time_zone = 'Central Time (US & Canada)'
17 |
18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
20 | # config.i18n.default_locale = :de
21 | config.autoload_paths += %W(#{Rails.root}/lib)
22 | end
23 | end
24 |
25 |
--------------------------------------------------------------------------------
/test/dummy/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 4f652b992e258c03478b463380b23bca70cb9758a3b26c6dd3f5c31a46fd042548ff0176cc6ad3a5e1077c675c6f8bb305a14121ca5c68568318461507d2c94b
15 |
16 | test:
17 | secret_key_base: 8302ca4ce082fa7e37dc5f8c5915609ee6e7c451fb8451aa2c87258f1db6d68759a2cbf50f358bf26847b57525b2caec1d916a7c51ed710b314067b44f25f864
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2014 YOURNAME
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/generators/form_generator_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'rails/generators/test_case'
3 | require 'rails/generators/form/form_generator'
4 |
5 | class FormGeneratorTest < Rails::Generators::TestCase
6 | tests Rails::Generators::FormGenerator
7 |
8 | destination File.expand_path("../../tmp", File.dirname(__FILE__))
9 | setup :prepare_destination
10 |
11 | def test_help
12 | content = run_generator ["--help"]
13 | assert_match(/creates an action form file/, content)
14 | end
15 |
16 | def test_form_is_created
17 | run_generator ["inquiry"]
18 | assert_file "app/forms/inquiry_form.rb", /class InquiryForm < ActionForm::Base/
19 | end
20 |
21 | def test_form_with_attributes
22 | run_generator ["feedback", "name", "email", "phone"]
23 | assert_file "app/forms/feedback_form.rb", /class FeedbackForm < ActionForm::Base/
24 | assert_file "app/forms/feedback_form.rb", /attributes :name, :email, :phone/
25 | end
26 |
27 | def test_namespaced_forms
28 | run_generator ["admin/feedback"]
29 | assert_file "app/forms/admin/feedback_form.rb", /class Admin::FeedbackForm < ActionForm::Base/
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/dummy/app/views/projects/_task_fields.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= f.input :name, :wrapper => 'inline', :label => false, :hint => 'name the task' do %>
6 | <%= f.input_field :name, :hint => 'type the name' %>
7 | <% end %>
8 |
9 |
10 |
11 | <%= f.input :description, :wrapper => 'inline', :label => false, :hint => 'describe the task' do %>
12 | <%= f.input_field :description, :hint => 'type the name' %>
13 | <% end %>
14 |
15 |
16 |
17 | <%= f.input :done, :as => :boolean %>
18 |
19 |
20 |
<%= link_to_remove_association "remove task", f, :confirm => 'do you really want to do this?' %>
44 | <% end %>
45 |
--------------------------------------------------------------------------------
/test/dummy/test/controllers/projects_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ProjectsControllerTest < ActionController::TestCase
4 | fixtures :projects
5 |
6 | setup do
7 | @project = projects(:yard)
8 | end
9 |
10 | test "should get index" do
11 | get :index
12 | assert_response :success
13 | assert_not_nil assigns(:projects)
14 | end
15 |
16 | test "should get new" do
17 | get :new
18 | assert_response :success
19 | end
20 |
21 | test "should create project" do
22 | assert_difference('Project.count') do
23 | post :create, project: { description: @project.description, name: @project.name }
24 | end
25 |
26 | assert_redirected_to project_path(assigns(:project))
27 | end
28 |
29 | test "should show project" do
30 | get :show, id: @project
31 | assert_response :success
32 | end
33 |
34 | test "should get edit" do
35 | get :edit, id: @project
36 | assert_response :success
37 | end
38 |
39 | test "should update project" do
40 | patch :update, id: @project, project: { description: @project.description, name: @project.name }
41 | assert_redirected_to project_path(assigns(:project))
42 | end
43 |
44 | test "should destroy project" do
45 | assert_difference('Project.count', -1) do
46 | delete :destroy, id: @project
47 | end
48 |
49 | assert_redirected_to projects_path
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports and disable caching.
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send.
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger.
20 | config.active_support.deprecation = :log
21 |
22 | # Raise an error on page load if there are pending migrations.
23 | config.active_record.migration_error = :page_load
24 |
25 | # Debug mode disables concatenation and preprocessing of assets.
26 | # This option may cause significant delays in view rendering with a large
27 | # number of complex assets.
28 | config.assets.debug = true
29 |
30 | # Adds additional error checking when serving assets at runtime.
31 | # Checks for improperly declared sprockets dependencies.
32 | # Raises helpful error messages.
33 | config.assets.raise_runtime_errors = true
34 |
35 | # Raises error for missing translations
36 | # config.action_view.raise_on_missing_translations = true
37 | end
38 |
--------------------------------------------------------------------------------
/test/dummy/app/views/projects/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | A project has a name, and an owner. It has many tasks, and many people as well.
6 | For sake of the example, we assume people only work on one project. While owners can have many projects.
7 | A project can have many tags. You can link an existing tag, or add a new tag and link to that.
8 |
43 | <% end %>
44 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static asset server for tests with Cache-Control for performance.
16 | config.serve_static_assets = true
17 | config.static_cache_control = 'public, max-age=3600'
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | config.action_mailer.delivery_method = :test
33 |
34 | # Print deprecation notices to the stderr.
35 | config.active_support.deprecation = :stderr
36 |
37 | # Raises error for missing translations
38 | # config.action_view.raise_on_missing_translations = true
39 | end
40 |
--------------------------------------------------------------------------------
/test/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :assignments
3 |
4 | resources :projects
5 |
6 | resources :surveys
7 |
8 | resources :conferences
9 |
10 | resources :songs
11 |
12 | resources :users
13 |
14 | # The priority is based upon order of creation: first created -> highest priority.
15 | # See how all your routes lay out with "rake routes".
16 |
17 | # You can have the root of your site routed with "root"
18 | # root 'welcome#index'
19 |
20 | # Example of regular route:
21 | # get 'products/:id' => 'catalog#view'
22 |
23 | # Example of named route that can be invoked with purchase_url(id: product.id)
24 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
25 |
26 | # Example resource route (maps HTTP verbs to controller actions automatically):
27 | # resources :products
28 |
29 | # Example resource route with options:
30 | # resources :products do
31 | # member do
32 | # get 'short'
33 | # post 'toggle'
34 | # end
35 | #
36 | # collection do
37 | # get 'sold'
38 | # end
39 | # end
40 |
41 | # Example resource route with sub-resources:
42 | # resources :products do
43 | # resources :comments, :sales
44 | # resource :seller
45 | # end
46 |
47 | # Example resource route with more complex sub-resources:
48 | # resources :products do
49 | # resources :comments
50 | # resources :sales do
51 | # get 'recent', on: :collection
52 | # end
53 | # end
54 |
55 | # Example resource route with concerns:
56 | # concern :toggleable do
57 | # post 'toggle'
58 | # end
59 | # resources :posts, concerns: :toggleable
60 | # resources :photos, concerns: :toggleable
61 |
62 | # Example resource route within a namespace:
63 | # namespace :admin do
64 | # # Directs /admin/products/* to Admin::ProductsController
65 | # # (app/controllers/admin/products_controller.rb)
66 | # resources :products
67 | # end
68 | end
69 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/assignments_controller.rb:
--------------------------------------------------------------------------------
1 | class AssignmentsController < ApplicationController
2 | before_action :set_assignment, only: [:show, :edit, :update, :destroy]
3 | before_action :create_new_form, only: [:new, :create]
4 | before_action :create_edit_form, only: [:edit, :update]
5 |
6 |
7 | def index
8 | @assignments = Assignment.all
9 | end
10 |
11 | def show
12 | end
13 |
14 | def new
15 | end
16 |
17 | def edit
18 | end
19 |
20 | def create
21 | @assignment_form.submit(assignment_params)
22 |
23 | respond_to do |format|
24 | if @assignment_form.save
25 | format.html { redirect_to @assignment_form, notice: "Assignment: #{@assignment_form.name} was successfully created." }
26 | else
27 | format.html { render :new }
28 | end
29 | end
30 | end
31 |
32 | def update
33 | @assignment_form.submit(assignment_params)
34 |
35 | respond_to do |format|
36 | if @assignment_form.save
37 | format.html { redirect_to @assignment_form, notice: "Assignment: #{@assignment_form.name} was successfully updated." }
38 | else
39 | format.html { render :edit }
40 | end
41 | end
42 | end
43 |
44 | def destroy
45 | name = @assignment.name
46 |
47 | @assignment.destroy
48 | respond_to do |format|
49 | format.html { redirect_to assignments_url, notice: "Assignment: #{name} was successfully destroyed." }
50 | format.json { head :no_content }
51 | end
52 | end
53 |
54 | private
55 | # Use callbacks to share common setup or constraints between actions.
56 | def set_assignment
57 | @assignment = Assignment.find(params[:id])
58 | end
59 |
60 | def create_new_form
61 | assignment = Assignment.new
62 | @assignment_form = AssignmentForm.new(assignment)
63 | end
64 |
65 | def create_edit_form
66 | @assignment_form = AssignmentForm.new(@assignment)
67 | end
68 |
69 | # Never trust parameters from the scary internet, only allow the white list through.
70 | def assignment_params
71 | params.require(:assignment).permit(:name, tasks_attributes: [:id, :name, :_destroy])
72 | end
73 | end
--------------------------------------------------------------------------------
/app/assets/javascripts/action_form.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 |
3 | var createNewResourceID = function() {
4 | return new Date().getTime();
5 | }
6 |
7 | $(document).on('click', '.add_fields', function(e) {
8 | e.preventDefault();
9 |
10 | var $link = $(this);
11 | var assoc = $link.data('association');
12 | var content = $link.data('association-insertion-template');
13 | var insertionMethod = $link.data('association-insertion-method') || $link.data('association-insertion-position') || 'before';
14 | var insertionNode = $link.data('association-insertion-node');
15 | var insertionTraversal = $link.data('association-insertion-traversal');
16 | var newId = createNewResourceID();
17 | var regex = new RegExp("new_" + assoc, "g");
18 | var newContent = content.replace(regex, newId);
19 |
20 | if (insertionNode){
21 | if (insertionTraversal){
22 | insertionNode = $link[insertionTraversal](insertionNode);
23 | } else {
24 | insertionNode = insertionNode == "this" ? $link : $(insertionNode);
25 | }
26 | } else {
27 | insertionNode = $link.parent();
28 | }
29 |
30 | var contentNode = $(newContent);
31 | insertionNode.trigger('before-insert', [contentNode]);
32 |
33 | var addedContent = insertionNode[insertionMethod](contentNode);
34 |
35 | insertionNode.trigger('after-insert', [contentNode]);
36 | });
37 |
38 | $(document).on('click', '.remove_fields.dynamic, .remove_fields.existing', function(e) {
39 | e.preventDefault();
40 |
41 | var $link = $(this);
42 | var wrapperClass = $link.data('wrapper-class') || 'nested-fields';
43 | var nodeToDelete = $link.closest('.' + wrapperClass);
44 | var triggerNode = nodeToDelete.parent();
45 |
46 | triggerNode.trigger('before-remove', [nodeToDelete]);
47 |
48 | var timeout = triggerNode.data('remove-timeout') || 0;
49 |
50 | setTimeout(function() {
51 | if ($link.hasClass('dynamic')) {
52 | nodeToDelete.remove();
53 | } else {
54 | $link.prev("input[type=hidden]").val("1");
55 | nodeToDelete.hide();
56 | }
57 | triggerNode.trigger('after-remove', [nodeToDelete]);
58 | }, timeout);
59 | });
60 |
61 | })(jQuery);
62 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | before_action :set_user, only: [:show, :edit, :update, :destroy]
3 | before_action :create_new_form, only: [:new, :create]
4 | before_action :create_edit_form, only: [:edit, :update]
5 |
6 | # GET /users
7 | # GET /users.json
8 | def index
9 | @users = User.all
10 | end
11 |
12 | # GET /users/1
13 | # GET /users/1.json
14 | def show
15 | end
16 |
17 | # GET /users/new
18 | def new
19 | end
20 |
21 | # GET /users/1/edit
22 | def edit
23 | end
24 |
25 | # POST /users
26 | # POST /users.json
27 | def create
28 | @user_form.submit(user_params)
29 |
30 | respond_to do |format|
31 | if @user_form.save
32 | format.html { redirect_to @user_form, notice: "User: #{@user_form.name} was successfully created." }
33 | else
34 | format.html { render :new }
35 | end
36 | end
37 | end
38 |
39 | # PATCH/PUT /users/1
40 | # PATCH/PUT /users/1.json
41 | def update
42 | @user_form.submit(user_params)
43 |
44 | respond_to do |format|
45 | if @user_form.save
46 | format.html { redirect_to @user_form, notice: "User: #{@user_form.name} was successfully updated." }
47 | else
48 | format.html { render :edit }
49 | end
50 | end
51 | end
52 |
53 | # DELETE /users/1
54 | # DELETE /users/1.json
55 | def destroy
56 | name = @user.name
57 | @user.destroy
58 | respond_to do |format|
59 | format.html { redirect_to users_url, notice: "User: #{name} was successfully destroyed." }
60 | end
61 | end
62 |
63 | private
64 | # Use callbacks to share common setup or constraints between actions.
65 | def set_user
66 | @user = User.find(params[:id])
67 | end
68 |
69 | def create_new_form
70 | user = User.new
71 | @user_form = UserForm.new(user)
72 | end
73 |
74 | def create_edit_form
75 | @user_form = UserForm.new(@user)
76 | end
77 |
78 | # Never trust parameters from the scary internet, only allow the white list through.
79 | def user_params
80 | params.require(:user).permit(:name, :age, :gender, email_attributes: [:id, :address],
81 | profile_attributes: [:id, :twitter_name, :github_name])
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/songs_controller.rb:
--------------------------------------------------------------------------------
1 | class SongsController < ApplicationController
2 | before_action :set_song, only: [:show, :edit, :update, :destroy]
3 | before_action :create_new_form, only: [:new, :create]
4 | before_action :create_edit_form, only: [:edit, :update]
5 |
6 | # GET /songs
7 | # GET /songs.json
8 | def index
9 | @songs = Song.all
10 | end
11 |
12 | # GET /songs/1
13 | # GET /songs/1.json
14 | def show
15 | end
16 |
17 | # GET /songs/new
18 | def new
19 | end
20 |
21 | # GET /songs/1/edit
22 | def edit
23 | end
24 |
25 | # POST /songs
26 | # POST /songs.json
27 | def create
28 | @song_form.submit(song_params)
29 |
30 | respond_to do |format|
31 | if @song_form.save
32 | format.html { redirect_to @song_form, notice: "Song: #{@song_form.title} was successfully created." }
33 | else
34 | format.html { render :new }
35 | end
36 | end
37 | end
38 |
39 | # PATCH/PUT /songs/1
40 | # PATCH/PUT /songs/1.json
41 | def update
42 | @song_form.submit(song_params)
43 |
44 | respond_to do |format|
45 | if @song_form.save
46 | format.html { redirect_to @song_form, notice: "Song: #{@song_form.title} was successfully updated." }
47 | else
48 | format.html { render :edit }
49 | end
50 | end
51 | end
52 |
53 | # DELETE /songs/1
54 | # DELETE /songs/1.json
55 | def destroy
56 | title = @song.title
57 | @song.destroy
58 |
59 | respond_to do |format|
60 | format.html { redirect_to songs_url, notice: "Song: #{title} was successfully destroyed." }
61 | format.json { head :no_content }
62 | end
63 | end
64 |
65 | private
66 | # Use callbacks to share common setup or constraints between actions.
67 | def set_song
68 | @song = Song.find(params[:id])
69 | end
70 |
71 | def create_new_form
72 | song = Song.new
73 | @song_form = SongForm.new(song)
74 | end
75 |
76 | def create_edit_form
77 | @song_form = SongForm.new(@song)
78 | end
79 |
80 | # Never trust parameters from the scary internet, only allow the white list through.
81 | def song_params
82 | params.require(:song).permit(:title, :length, artist_attributes:
83 | [:name, producer_attributes: [ :name, :studio ] ] )
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/action_form/view_helpers.rb:
--------------------------------------------------------------------------------
1 | module ActionForm
2 | module ViewHelpers
3 |
4 | def link_to_remove_association(name, f, html_options={})
5 | classes = []
6 | classes << "remove_fields"
7 |
8 | is_existing = f.object.persisted?
9 | classes << (is_existing ? 'existing' : 'dynamic')
10 |
11 | wrapper_class = html_options.delete(:wrapper_class)
12 | html_options[:class] = [html_options[:class], classes.join(' ')].compact.join(' ')
13 | html_options[:'data-wrapper-class'] = wrapper_class if wrapper_class.present?
14 |
15 | if is_existing
16 | f.hidden_field(:_destroy) + link_to(name, '#', html_options)
17 | else
18 | link_to(name, '#', html_options)
19 | end
20 | end
21 |
22 | def render_association(association, f, new_object, render_options={}, custom_partial=nil)
23 | partial = get_partial_path(custom_partial, association)
24 |
25 | if f.respond_to?(:semantic_fields_for)
26 | method_name = :semantic_fields_for
27 | elsif f.respond_to?(:simple_fields_for)
28 | method_name = :simple_fields_for
29 | else
30 | method_name = :fields_for
31 | end
32 |
33 | f.send(method_name, association, new_object, {:child_index => "new_#{association}"}.merge(render_options)) do |builder|
34 | render(partial: partial, locals: {:f => builder})
35 | end
36 | end
37 |
38 | def link_to_add_association(name, f, association, html_options={})
39 | render_options = html_options.delete(:render_options)
40 | render_options ||= {}
41 | override_partial = html_options.delete(:partial)
42 |
43 | html_options[:class] = [html_options[:class], "add_fields"].compact.join(' ')
44 | html_options[:'data-association'] = association.to_s
45 |
46 | new_object = create_object(f, association)
47 |
48 | html_options[:'data-association-insertion-template'] = CGI.escapeHTML(render_association(association, f, new_object, render_options, override_partial).to_str).html_safe
49 |
50 | link_to(name, '#', html_options)
51 | end
52 |
53 | def create_object(f, association)
54 | f.object.get_model(association)
55 | end
56 |
57 | def get_partial_path(partial, association)
58 | partial ? partial : association.to_s.singularize + "_fields"
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/surveys_controller.rb:
--------------------------------------------------------------------------------
1 | class SurveysController < ApplicationController
2 | before_action :set_survey, only: [:show, :edit, :update, :destroy]
3 | before_action :create_new_form, only: [:new, :create]
4 | before_action :create_edit_form, only: [:edit, :update]
5 |
6 | # GET /surveys
7 | # GET /surveys.json
8 | def index
9 | @surveys = Survey.all
10 | end
11 |
12 | # GET /surveys/1
13 | # GET /surveys/1.json
14 | def show
15 | end
16 |
17 | # GET /surveys/new
18 | def new
19 | end
20 |
21 | # GET /surveys/1/edit
22 | def edit
23 | end
24 |
25 | # POST /surveys
26 | # POST /surveys.json
27 | def create
28 | @survey_form.submit(survey_params)
29 |
30 | respond_to do |format|
31 | if @survey_form.save
32 | format.html { redirect_to @survey_form, notice: "Survey: #{@survey_form.name} was successfully created." }
33 | else
34 | format.html { render :new }
35 | end
36 | end
37 | end
38 |
39 | # PATCH/PUT /surveys/1
40 | # PATCH/PUT /surveys/1.json
41 | def update
42 | @survey_form.submit(survey_params)
43 |
44 | respond_to do |format|
45 | if @survey_form.save
46 | format.html { redirect_to @survey_form, notice: "Survey: #{@survey_form.name} was successfully updated." }
47 | else
48 | format.html { render :edit }
49 | end
50 | end
51 | end
52 |
53 | # DELETE /surveys/1
54 | # DELETE /surveys/1.json
55 | def destroy
56 | name = @survey.name
57 |
58 | @survey.destroy
59 | respond_to do |format|
60 | format.html { redirect_to surveys_url, notice: "Survey: #{name} was successfully destroyed." }
61 | end
62 | end
63 |
64 | private
65 | # Use callbacks to share common setup or constraints between actions.
66 | def set_survey
67 | @survey = Survey.find(params[:id])
68 | end
69 |
70 | def create_new_form
71 | survey = Survey.new
72 | @survey_form = SurveyForm.new(survey)
73 | end
74 |
75 | def create_edit_form
76 | @survey_form = SurveyForm.new(@survey)
77 | end
78 |
79 | # Never trust parameters from the scary internet, only allow the white list through.
80 | def survey_params
81 | params.require(:survey).permit(:name, questions_attributes: [:id, :_destroy, :content,
82 | answers_attributes: [:id, :_destroy, :content]])
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | actionform (0.0.1)
5 | rails (~> 4.1)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | actionmailer (4.1.8)
11 | actionpack (= 4.1.8)
12 | actionview (= 4.1.8)
13 | mail (~> 2.5, >= 2.5.4)
14 | actionpack (4.1.8)
15 | actionview (= 4.1.8)
16 | activesupport (= 4.1.8)
17 | rack (~> 1.5.2)
18 | rack-test (~> 0.6.2)
19 | actionview (4.1.8)
20 | activesupport (= 4.1.8)
21 | builder (~> 3.1)
22 | erubis (~> 2.7.0)
23 | activemodel (4.1.8)
24 | activesupport (= 4.1.8)
25 | builder (~> 3.1)
26 | activerecord (4.1.8)
27 | activemodel (= 4.1.8)
28 | activesupport (= 4.1.8)
29 | arel (~> 5.0.0)
30 | activesupport (4.1.8)
31 | i18n (~> 0.6, >= 0.6.9)
32 | json (~> 1.7, >= 1.7.7)
33 | minitest (~> 5.1)
34 | thread_safe (~> 0.1)
35 | tzinfo (~> 1.1)
36 | arel (5.0.1.20140414130214)
37 | builder (3.2.2)
38 | erubis (2.7.0)
39 | hike (1.2.3)
40 | i18n (0.6.11)
41 | jquery-rails (3.1.1)
42 | railties (>= 3.0, < 5.0)
43 | thor (>= 0.14, < 2.0)
44 | json (1.8.1)
45 | mail (2.6.3)
46 | mime-types (>= 1.16, < 3)
47 | mime-types (2.4.3)
48 | minitest (5.5.0)
49 | multi_json (1.10.1)
50 | rack (1.5.2)
51 | rack-test (0.6.2)
52 | rack (>= 1.0)
53 | rails (4.1.8)
54 | actionmailer (= 4.1.8)
55 | actionpack (= 4.1.8)
56 | actionview (= 4.1.8)
57 | activemodel (= 4.1.8)
58 | activerecord (= 4.1.8)
59 | activesupport (= 4.1.8)
60 | bundler (>= 1.3.0, < 2.0)
61 | railties (= 4.1.8)
62 | sprockets-rails (~> 2.0)
63 | railties (4.1.8)
64 | actionpack (= 4.1.8)
65 | activesupport (= 4.1.8)
66 | rake (>= 0.8.7)
67 | thor (>= 0.18.1, < 2.0)
68 | rake (10.3.2)
69 | simple_form (3.0.2)
70 | actionpack (~> 4.0)
71 | activemodel (~> 4.0)
72 | sprockets (2.12.3)
73 | hike (~> 1.2)
74 | multi_json (~> 1.0)
75 | rack (~> 1.0)
76 | tilt (~> 1.1, != 1.3.0)
77 | sprockets-rails (2.2.4)
78 | actionpack (>= 3.0)
79 | activesupport (>= 3.0)
80 | sprockets (>= 2.8, < 4.0)
81 | sqlite3 (1.3.9)
82 | thor (0.19.1)
83 | thread_safe (0.3.4)
84 | tilt (1.4.1)
85 | tzinfo (1.2.2)
86 | thread_safe (~> 0.1)
87 |
88 | PLATFORMS
89 | ruby
90 |
91 | DEPENDENCIES
92 | actionform!
93 | jquery-rails
94 | rake (~> 10.3.2)
95 | simple_form
96 | sqlite3
97 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/conferences_controller.rb:
--------------------------------------------------------------------------------
1 | class ConferencesController < ApplicationController
2 | before_action :set_conference, only: [:show, :edit, :update, :destroy]
3 | before_action :create_new_form, only: [:new, :create]
4 | before_action :create_edit_form, only: [:edit, :update]
5 |
6 | # GET /conferences
7 | # GET /conferences.json
8 | def index
9 | @conferences = Conference.all
10 | end
11 |
12 | # GET /conferences/1
13 | # GET /conferences/1.json
14 | def show
15 | end
16 |
17 | # GET /conferences/new
18 | def new
19 | end
20 |
21 | # GET /conferences/1/edit
22 | def edit
23 | end
24 |
25 | # POST /conferences
26 | # POST /conferences.json
27 | def create
28 | @conference_form.submit(conference_params)
29 |
30 | respond_to do |format|
31 | if @conference_form.save
32 | format.html { redirect_to @conference_form, notice: "Conference: #{@conference_form.name} was successfully created." }
33 | else
34 | format.html { render :new }
35 | end
36 | end
37 | end
38 |
39 | # PATCH/PUT /conferences/1
40 | # PATCH/PUT /conferences/1.json
41 | def update
42 | @conference_form.submit(conference_params)
43 |
44 | respond_to do |format|
45 | if @conference_form.save
46 | format.html { redirect_to @conference_form, notice: "Conference: #{@conference_form.name} was successfully updated." }
47 | else
48 | format.html { render :edit }
49 | end
50 | end
51 | end
52 |
53 | # DELETE /conferences/1
54 | # DELETE /conferences/1.json
55 | def destroy
56 | name = @conference.name
57 |
58 | @conference.destroy
59 | respond_to do |format|
60 | format.html { redirect_to conferences_url, notice: "Conference: #{name} was successfully destroyed." }
61 | end
62 | end
63 |
64 | private
65 | # Use callbacks to share common setup or constraints between actions.
66 | def set_conference
67 | @conference = Conference.find(params[:id])
68 | end
69 |
70 | def create_new_form
71 | conference = Conference.new
72 | @conference_form = ConferenceForm.new(conference)
73 | end
74 |
75 | def create_edit_form
76 | @conference_form = ConferenceForm.new(@conference)
77 | end
78 |
79 | # Never trust parameters from the scary internet, only allow the white list through.
80 | def conference_params
81 | params.require(:conference).permit(:name, :city, speaker_attributes: [:id, :name, :occupation,
82 | presentations_attributes: [:id, :_destroy, :topic, :duration]])
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/projects_controller.rb:
--------------------------------------------------------------------------------
1 | class ProjectsController < ApplicationController
2 | before_action :set_project, only: [:show, :edit, :update, :destroy]
3 |
4 | # GET /projects
5 | # GET /projects.json
6 | def index
7 | @projects = Project.all
8 | end
9 |
10 | # GET /projects/1
11 | # GET /projects/1.json
12 | def show
13 | end
14 |
15 | # GET /projects/new
16 | def new
17 | @project = Project.new
18 | @project_form = ProjectForm.new(@project)
19 | end
20 |
21 | # GET /projects/1/edit
22 | def edit
23 | @project_form = ProjectForm.new(@project)
24 | end
25 |
26 | # POST /projects
27 | # POST /projects.json
28 | def create
29 | @project = Project.new
30 | @project_form = ProjectForm.new(@project)
31 |
32 | @project_form.submit(project_params)
33 |
34 | respond_to do |format|
35 | if @project_form.save
36 | format.html { redirect_to @project_form, notice: 'Project was successfully created.' }
37 | else
38 | format.html { render :new }
39 | end
40 | end
41 | end
42 |
43 | # PATCH/PUT /projects/1
44 | # PATCH/PUT /projects/1.json
45 | def update
46 | @project_form = ProjectForm.new(@project)
47 |
48 | @project_form.submit(project_params)
49 |
50 | respond_to do |format|
51 | if @project_form.save
52 | format.html { redirect_to @project_form, notice: 'Project was successfully updated.' }
53 | else
54 | format.html { render :edit }
55 | end
56 | end
57 | end
58 |
59 | # DELETE /projects/1
60 | # DELETE /projects/1.json
61 | def destroy
62 | @project.destroy
63 | respond_to do |format|
64 | format.html { redirect_to projects_url, notice: 'Project was successfully destroyed.' }
65 | end
66 | end
67 |
68 | private
69 | # Use callbacks to share common setup or constraints between actions.
70 | def set_project
71 | @project = Project.find(params[:id])
72 | end
73 |
74 | # Never trust parameters from the scary internet, only allow the white list through.
75 | def project_params
76 | params.require(:project).permit(:name, :owner_id, tasks_attributes: [ :name, :description, :done, :id, :_destroy,
77 | sub_tasks_attributes: [ :name, :description, :done, :id, :_destroy ] ],
78 | owner_attributes: [ :name, :role, :description, :id, :_destroy ],
79 | contributors_attributes: [ :name, :role, :description, :id, :_destroy ],
80 | project_tags_attributes: [ :tag_id, :id, :_destroy, tag_attributes:
81 | [ :name, :id, :_destroy ] ])
82 | end
83 |
84 | end
--------------------------------------------------------------------------------
/lib/action_form/base.rb:
--------------------------------------------------------------------------------
1 | module ActionForm
2 | class Base
3 | include ActiveModel::Model
4 | include FormHelpers
5 | extend ActiveModel::Callbacks
6 |
7 | define_model_callbacks :save, only: [:after]
8 | after_save :update_form_models
9 |
10 | delegate :persisted?, :to_model, :to_key, :to_param, :to_partial_path, to: :model
11 | attr_reader :model, :forms
12 |
13 | def initialize(model)
14 | @model = model
15 | @forms = []
16 | populate_forms
17 | end
18 |
19 | def get_model(assoc_name)
20 | form = find_form_by_assoc_name(assoc_name)
21 | form.get_model(assoc_name)
22 | end
23 |
24 | def save
25 | if valid?
26 | run_callbacks :save do
27 | ActiveRecord::Base.transaction do
28 | model.save
29 | end
30 | end
31 | else
32 | false
33 | end
34 | end
35 |
36 | class << self
37 | attr_writer :main_class, :main_model
38 | delegate :reflect_on_association, to: :main_class
39 |
40 | def attributes(*names)
41 | options = names.pop if names.last.is_a?(Hash)
42 |
43 | if options && options[:required]
44 | validates_presence_of(*names)
45 | end
46 |
47 | names.each do |attribute|
48 | delegate attribute, "#{attribute}=", to: :model
49 | end
50 | end
51 |
52 | def main_class
53 | @main_class ||= main_model.to_s.camelize.constantize
54 | end
55 |
56 | def main_model
57 | @main_model ||= name.sub(/Form$/, '').singularize
58 | end
59 |
60 | alias_method :attribute, :attributes
61 |
62 | def association(name, options={}, &block)
63 | forms << FormDefinition.new(name, block, options)
64 | macro = main_class.reflect_on_association(name).macro
65 |
66 | case macro
67 | when :has_one, :belongs_to
68 | class_eval "def #{name}; @#{name}; end"
69 | when :has_many
70 | class_eval "def #{name}; @#{name}.models; end"
71 | end
72 |
73 | class_eval "def #{name}_attributes=; end"
74 | end
75 |
76 | def forms
77 | @forms ||= []
78 | end
79 | end
80 |
81 | private
82 |
83 | def update_form_models
84 | forms.each do |form|
85 | form.update_models
86 | end
87 | end
88 |
89 | def populate_forms
90 | self.class.forms.each do |definition|
91 | definition.parent = model
92 | nested_form = definition.to_form
93 | forms << nested_form
94 | name = definition.assoc_name
95 | instance_variable_set("@#{name}", nested_form)
96 | end
97 | end
98 |
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
20 | # config.action_dispatch.rack_cache = true
21 |
22 | # Disable Rails's static asset server (Apache or nginx will already do this).
23 | config.serve_static_assets = false
24 |
25 | # Compress JavaScripts and CSS.
26 | config.assets.js_compressor = :uglifier
27 | # config.assets.css_compressor = :sass
28 |
29 | # Do not fallback to assets pipeline if a precompiled asset is missed.
30 | config.assets.compile = false
31 |
32 | # Generate digests for assets URLs.
33 | config.assets.digest = true
34 |
35 | # `config.assets.precompile` has moved to config/initializers/assets.rb
36 |
37 | # Specifies the header that your server uses for sending files.
38 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
40 |
41 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
42 | # config.force_ssl = true
43 |
44 | # Set to :debug to see everything in the log.
45 | config.log_level = :info
46 |
47 | # Prepend all log lines with the following tags.
48 | # config.log_tags = [ :subdomain, :uuid ]
49 |
50 | # Use a different logger for distributed setups.
51 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
52 |
53 | # Use a different cache store in production.
54 | # config.cache_store = :mem_cache_store
55 |
56 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
57 | # config.action_controller.asset_host = "http://assets.example.com"
58 |
59 | # Precompile additional assets.
60 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
61 | # config.assets.precompile += %w( search.js )
62 |
63 | # Ignore bad email addresses and do not raise email delivery errors.
64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
65 | # config.action_mailer.raise_delivery_errors = false
66 |
67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
68 | # the I18n.default_locale when a translation cannot be found).
69 | config.i18n.fallbacks = true
70 |
71 | # Send deprecation notices to registered listeners.
72 | config.active_support.deprecation = :notify
73 |
74 | # Disable automatic flushing of the log to improve performance.
75 | # config.autoflush_log = false
76 |
77 | # Use default logging formatter so that PID and timestamp are not suppressed.
78 | config.log_formatter = ::Logger::Formatter.new
79 |
80 | # Do not dump schema after migrations.
81 | config.active_record.dump_schema_after_migration = false
82 | end
83 |
--------------------------------------------------------------------------------
/test/dummy/test/controllers/assignments_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class AssignmentsControllerTest < ActionController::TestCase
4 | fixtures :assignments, :tasks
5 |
6 | setup do
7 | @assignment = assignments(:yard)
8 | end
9 |
10 | test "should get index" do
11 | get :index
12 | assert_response :success
13 | assert_not_nil assigns(:assignments)
14 | end
15 |
16 | test "should get new" do
17 | get :new
18 | assert_response :success
19 | end
20 |
21 | test "should create assignment" do
22 | assert_difference('Assignment.count') do
23 | post :create, assignment: {
24 | name: "Life",
25 |
26 | tasks_attributes: {
27 | "0" => { name: "Eat" },
28 | "1" => { name: "Pray" },
29 | "2" => { name: "Love" },
30 | }
31 | }
32 | end
33 |
34 | assignment_form = assigns(:assignment_form)
35 |
36 | assert assignment_form.valid?
37 | assert_redirected_to assignment_path(assignment_form)
38 |
39 | assert_equal "Life", assignment_form.name
40 |
41 | assert_equal "Eat", assignment_form.tasks[0].name
42 | assert_equal "Pray", assignment_form.tasks[1].name
43 | assert_equal "Love", assignment_form.tasks[2].name
44 |
45 | assignment_form.tasks.each do |task_form|
46 | assert task_form.persisted?
47 | end
48 |
49 | assert_equal "Assignment: Life was successfully created.", flash[:notice]
50 | end
51 |
52 | test "should not create assignment with invalid params" do
53 | assignment = assignments(:yard)
54 |
55 | assert_difference('Assignment.count', 0) do
56 | post :create, assignment: {
57 | name: assignment.name,
58 |
59 | tasks_attributes: {
60 | "0" => { name: nil },
61 | "1" => { name: nil },
62 | "2" => { name: nil },
63 | }
64 | }
65 | end
66 |
67 | assignment_form = assigns(:assignment_form)
68 |
69 | assert_not assignment_form.valid?
70 | assert_includes assignment_form.errors.messages[:name], "has already been taken"
71 |
72 | assignment_form.tasks.each do |task_form|
73 | assert_includes task_form.errors.messages[:name], "can't be blank"
74 | end
75 | end
76 |
77 | test "should show assignment" do
78 | get :show, id: @assignment
79 | assert_response :success
80 | end
81 |
82 | test "should get edit" do
83 | get :edit, id: @assignment
84 | assert_response :success
85 | end
86 |
87 | test "should update assignment" do
88 | assert_difference('Assignment.count', 0) do
89 | patch :update, id: @assignment, assignment: {
90 | name: "Car service",
91 |
92 | tasks_attributes: {
93 | "0" => { name: "Wash tires", id: tasks(:rake).id },
94 | "1" => { name: "Clean inside", id: tasks(:paint).id },
95 | "2" => { name: "Check breaks", id: tasks(:clean).id },
96 | }
97 | }
98 | end
99 |
100 | assignment_form = assigns(:assignment_form)
101 |
102 | assert_redirected_to assignment_path(assignment_form)
103 |
104 | assert_equal "Car service", assignment_form.name
105 |
106 | assert_equal "Wash tires", assignment_form.tasks[0].name
107 | assert_equal "Clean inside", assignment_form.tasks[1].name
108 | assert_equal "Check breaks", assignment_form.tasks[2].name
109 |
110 | assert_equal "Assignment: Car service was successfully updated.", flash[:notice]
111 | end
112 |
113 | test "should destroy assignment" do
114 | assert_difference('Assignment.count', -1) do
115 | delete :destroy, id: @assignment
116 | end
117 |
118 | assert_redirected_to assignments_path
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/test/dummy/test/controllers/users_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class UsersControllerTest < ActionController::TestCase
4 | fixtures :users, :emails, :profiles
5 |
6 | setup do
7 | @user = users(:peter)
8 | end
9 |
10 | test "should get index" do
11 | get :index
12 | assert_response :success
13 | assert_not_nil assigns(:users)
14 | end
15 |
16 | test "should get new" do
17 | get :new
18 | assert_response :success
19 | end
20 |
21 | test "should create user" do
22 | assert_difference(['User.count', 'Email.count', 'Profile.count']) do
23 | post :create, user: {
24 | age: "23",
25 | gender: "0",
26 | name: "petrakos",
27 |
28 | email_attributes: {
29 | address: "petrakos@gmail.com"
30 | },
31 |
32 | profile_attributes: {
33 | twitter_name: "t_peter",
34 | github_name: "g_peter"
35 | }
36 | }
37 | end
38 |
39 | user_form = assigns(:user_form)
40 |
41 | assert user_form.valid?
42 | assert_redirected_to user_path(user_form)
43 |
44 | assert_equal "petrakos", user_form.name
45 | assert_equal 23, user_form.age
46 | assert_equal 0, user_form.gender
47 |
48 | assert_equal "petrakos@gmail.com", user_form.email.address
49 |
50 | assert_equal "t_peter", user_form.profile.twitter_name
51 | assert_equal "g_peter", user_form.profile.github_name
52 |
53 | assert_equal "User: #{user_form.name} was successfully created.", flash[:notice]
54 | end
55 |
56 | test "should not create user with invalid params" do
57 | peter = users(:peter)
58 |
59 | assert_difference(['User.count', 'Email.count', 'Profile.count'], 0) do
60 | post :create, user: {
61 | name: peter.name,
62 | age: nil,
63 | gender: "0",
64 |
65 | email_attributes: {
66 | address: peter.email.address
67 | },
68 |
69 | profile_attributes: {
70 | twitter_name: peter.profile.twitter_name,
71 | github_name: peter.profile.github_name
72 | }
73 | }
74 | end
75 |
76 | user_form = assigns(:user_form)
77 |
78 | assert_not user_form.valid?
79 |
80 | assert_includes user_form.errors.messages[:name], "has already been taken"
81 | assert_includes user_form.errors.messages[:age], "can't be blank"
82 |
83 | assert_includes user_form.email.errors.messages[:address], "has already been taken"
84 |
85 | assert_includes user_form.profile.errors.messages[:twitter_name], "has already been taken"
86 | assert_includes user_form.profile.errors.messages[:github_name], "has already been taken"
87 | end
88 |
89 | test "should show user" do
90 | get :show, id: @user
91 | assert_response :success
92 | end
93 |
94 | test "should get edit" do
95 | get :edit, id: @user
96 | assert_response :success
97 | end
98 |
99 | test "should update user" do
100 | assert_difference(['User.count', 'Email.count', 'Profile.count'], 0) do
101 | patch :update, id: @user, user: {
102 | age: @user.age,
103 | gender: @user.gender,
104 | name: "petrakos",
105 |
106 | email_attributes: {
107 | address: "petrakos@gmail.com"
108 | },
109 |
110 | profile_attributes: {
111 | twitter_name: "t_peter",
112 | github_name: "g_peter"
113 | }
114 | }
115 | end
116 |
117 | user_form = assigns(:user_form)
118 |
119 | assert_redirected_to user_path(user_form)
120 |
121 | assert_equal "petrakos", user_form.name
122 |
123 | assert_equal "petrakos@gmail.com", user_form.email.address
124 |
125 | assert_equal "t_peter", user_form.profile.twitter_name
126 | assert_equal "g_peter", user_form.profile.github_name
127 |
128 | assert_equal "User: #{user_form.name} was successfully updated.", flash[:notice]
129 | end
130 |
131 | test "should destroy user" do
132 | assert_difference('User.count', -1) do
133 | delete :destroy, id: @user
134 | end
135 |
136 | assert_redirected_to users_path
137 | end
138 | end
139 |
--------------------------------------------------------------------------------
/test/dummy/test/controllers/songs_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class SongsControllerTest < ActionController::TestCase
4 | fixtures :songs, :artists, :producers
5 |
6 | setup do
7 | @song = songs(:lockdown)
8 | end
9 |
10 | test "should get index" do
11 | get :index
12 | assert_response :success
13 | assert_not_nil assigns(:songs)
14 | end
15 |
16 | test "should get new" do
17 | get :new
18 | assert_response :success
19 | end
20 |
21 | test "should create song" do
22 | assert_difference(['Song.count', 'Artist.count', 'Producer.count']) do
23 | post :create, song: {
24 | title: "Diamonds",
25 | length: "360",
26 |
27 | artist_attributes: {
28 | name: "Karras",
29 |
30 | producer_attributes: {
31 | name: "Phoebos",
32 | studio: "MADog"
33 | }
34 | }
35 | }
36 | end
37 |
38 | song_form = assigns(:song_form)
39 |
40 | assert song_form.valid?
41 | assert_redirected_to song_path(song_form)
42 |
43 | assert_equal "Diamonds", song_form.title
44 | assert_equal "360", song_form.length
45 |
46 | assert_equal "Karras", song_form.artist.name
47 |
48 | assert_equal "Phoebos", song_form.artist.producer.name
49 | assert_equal "MADog", song_form.artist.producer.studio
50 |
51 | assert song_form.artist.persisted?
52 | assert song_form.artist.producer.persisted?
53 |
54 | assert_equal "Song: Diamonds was successfully created.", flash[:notice]
55 | end
56 |
57 | test "should not create song with invalid params" do
58 | assert_difference(['Song.count', 'Artist.count', 'Producer.count'], 0) do
59 | post :create, song: {
60 | title: nil,
61 | length: nil,
62 |
63 | artist_attributes: {
64 | name: nil,
65 |
66 | producer_attributes: {
67 | name: nil,
68 | studio: nil
69 | }
70 | }
71 | }
72 | end
73 |
74 | song_form = assigns(:song_form)
75 |
76 | assert_not song_form.valid?
77 |
78 | assert_includes song_form.errors[:title], "can't be blank"
79 | assert_includes song_form.errors[:length], "can't be blank"
80 |
81 | assert_includes song_form.errors["artist.name"], "can't be blank"
82 | assert_includes song_form.artist.errors[:name], "can't be blank"
83 |
84 | assert_includes song_form.artist.producer.errors[:name], "can't be blank"
85 | assert_includes song_form.artist.producer.errors[:studio], "can't be blank"
86 |
87 | assert_includes song_form.errors["artist.producer.name"], "can't be blank"
88 | assert_includes song_form.errors["artist.producer.studio"], "can't be blank"
89 | end
90 |
91 | test "should show song" do
92 | get :show, id: @song
93 | assert_response :success
94 | end
95 |
96 | test "should get edit" do
97 | get :edit, id: @song
98 | assert_response :success
99 | end
100 |
101 | test "should update song" do
102 | assert_difference(['Song.count', 'Artist.count', 'Producer.count'], 0) do
103 | patch :update, id: @song, song: {
104 | title: "Run this town",
105 | length: "355",
106 |
107 | artist_attributes: {
108 | name: "Rihanna",
109 |
110 | producer_attributes: {
111 | name: "Eminem",
112 | studio: "Marshall"
113 | }
114 | }
115 | }
116 | end
117 |
118 | song_form = assigns(:song_form)
119 |
120 | assert_redirected_to song_path(song_form)
121 |
122 | assert_equal "Run this town", song_form.title
123 | assert_equal "355", song_form.length
124 |
125 | assert_equal "Rihanna", song_form.artist.name
126 |
127 | assert_equal "Eminem", song_form.artist.producer.name
128 | assert_equal "Marshall", song_form.artist.producer.studio
129 |
130 | assert_equal "Song: Run this town was successfully updated.", flash[:notice]
131 | end
132 |
133 | test "should destroy song" do
134 | assert_difference('Song.count', -1) do
135 | delete :destroy, id: @song
136 | end
137 |
138 | assert_redirected_to songs_path
139 | end
140 | end
141 |
--------------------------------------------------------------------------------
/lib/action_form/form.rb:
--------------------------------------------------------------------------------
1 | module ActionForm
2 | class Form
3 | include ActiveModel::Validations
4 | include FormHelpers
5 |
6 | delegate :id, :_destroy, :persisted?, to: :model
7 | attr_reader :association_name, :parent, :model, :forms, :proc
8 |
9 | def initialize(assoc_name, parent, proc, model=nil)
10 | @association_name = assoc_name
11 | @parent = parent
12 | @model = assign_model(model)
13 | @forms = []
14 | @proc = proc
15 | enable_autosave
16 | instance_eval &proc
17 | end
18 |
19 | def class
20 | model.class
21 | end
22 |
23 | def association(name, options={}, &block)
24 | macro = model.class.reflect_on_association(name).macro
25 | form_definition = FormDefinition.new(name, block, options)
26 | form_definition.parent = @model
27 |
28 | case macro
29 | when :has_one, :belongs_to
30 | class_eval "def #{name}; @#{name}; end"
31 | when :has_many
32 | class_eval "def #{name}; @#{name}.models; end"
33 | end
34 |
35 | nested_form = form_definition.to_form
36 | @forms << nested_form
37 | instance_variable_set("@#{name}", nested_form)
38 |
39 | class_eval "def #{name}_attributes=; end"
40 | end
41 |
42 | def attributes(*arguments)
43 | class_eval do
44 | options = arguments.pop if arguments.last.is_a?(Hash)
45 |
46 | if options && options[:required]
47 | validates_presence_of(*arguments)
48 | end
49 |
50 | arguments.each do |attribute|
51 | delegate attribute, "#{attribute}=", to: :model
52 | end
53 | end
54 | end
55 |
56 | alias_method :attribute, :attributes
57 |
58 | def method_missing(method_sym, *arguments, &block)
59 | if method_sym =~ /^validates?$/
60 | class_eval do
61 | send(method_sym, *arguments, &block)
62 | end
63 | end
64 | end
65 |
66 | def update_models
67 | @model = parent.send("#{association_name}")
68 | end
69 |
70 | REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
71 |
72 | def call_reject_if(attributes)
73 | REJECT_ALL_BLANK_PROC.call(attributes)
74 | end
75 |
76 | def params_for_current_scope(attributes)
77 | attributes.dup.reject { |_, v| v.is_a? Hash }
78 | end
79 |
80 | def submit(params)
81 | reflection = association_reflection
82 |
83 | if reflection.macro == :belongs_to
84 | @model = parent.send("build_#{association_name}") unless call_reject_if(params_for_current_scope(params))
85 | end
86 |
87 | super
88 | end
89 |
90 | def get_model(assoc_name)
91 | if represents?(assoc_name)
92 | Form.new(association_name, parent, proc)
93 | else
94 | form = find_form_by_assoc_name(assoc_name)
95 | form.get_model(assoc_name)
96 | end
97 | end
98 |
99 | def delete
100 | model.mark_for_destruction
101 | end
102 |
103 | def represents?(assoc_name)
104 | association_name.to_s == assoc_name.to_s
105 | end
106 |
107 | private
108 |
109 | def enable_autosave
110 | reflection = association_reflection
111 | reflection.autosave = true
112 | end
113 |
114 | def association_reflection
115 | parent.class.reflect_on_association(association_name)
116 | end
117 |
118 | def build_model
119 | macro = association_reflection.macro
120 |
121 | case macro
122 | when :belongs_to
123 | if parent.send("#{association_name}")
124 | parent.send("#{association_name}")
125 | else
126 | association_reflection.klass.new
127 | end
128 | when :has_one
129 | fetch_or_initialize_model
130 | when :has_many
131 | parent.send(association_name).build
132 | end
133 | end
134 |
135 | def fetch_or_initialize_model
136 | if parent.send("#{association_name}")
137 | parent.send("#{association_name}")
138 | else
139 | parent.send("build_#{association_name}")
140 | end
141 | end
142 |
143 | def assign_model(model)
144 | if model
145 | model
146 | else
147 | build_model
148 | end
149 | end
150 |
151 | end
152 | end
153 |
--------------------------------------------------------------------------------
/lib/action_form/form_collection.rb:
--------------------------------------------------------------------------------
1 | module ActionForm
2 | class FormCollection
3 | include ActiveModel::Validations
4 |
5 | attr_reader :association_name, :records, :parent, :proc, :forms
6 |
7 | def initialize(assoc_name, parent, proc, options)
8 | @association_name = assoc_name
9 | @parent = parent
10 | @proc = proc
11 | @records = options[:records] || 1
12 | @forms = []
13 | assign_forms
14 | end
15 |
16 | def update_models
17 | @forms = []
18 | fetch_models
19 | end
20 |
21 | def submit(params)
22 | params.each do |key, value|
23 | if parent.persisted?
24 | create_or_update_record(value)
25 | else
26 | create_or_assign_record(key, value)
27 | end
28 | end
29 | end
30 |
31 | def get_model(assoc_name)
32 | Form.new(association_name, parent, proc)
33 | end
34 |
35 | def valid?
36 | aggregate_form_errors
37 |
38 | errors.empty?
39 | end
40 |
41 | def represents?(assoc_name)
42 | association_name.to_s == assoc_name.to_s
43 | end
44 |
45 | def models
46 | forms
47 | end
48 |
49 | def each(&block)
50 | forms.each do |form|
51 | block.call(form)
52 | end
53 | end
54 |
55 | private
56 |
57 | REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
58 |
59 | UNASSIGNABLE_KEYS = %w( id _destroy )
60 |
61 | def call_reject_if(attributes)
62 | REJECT_ALL_BLANK_PROC.call(attributes)
63 | end
64 |
65 | def assign_to_or_mark_for_destruction(form, attributes)
66 | form.submit(attributes.except(*UNASSIGNABLE_KEYS))
67 |
68 | if has_destroy_flag?(attributes)
69 | form.delete
70 | remove_form(form)
71 | end
72 | end
73 |
74 | def existing_record?(attributes)
75 | attributes[:id] != nil
76 | end
77 |
78 | def update_record(attributes)
79 | id = attributes[:id]
80 | form = find_form_by_model_id(id)
81 | assign_to_or_mark_for_destruction(form, attributes)
82 | end
83 |
84 | def create_record(attributes)
85 | new_form = create_form
86 | new_form.submit(attributes)
87 | end
88 |
89 | def create_or_update_record(attributes)
90 | if existing_record?(attributes)
91 | update_record(attributes)
92 | else
93 | create_record(attributes)
94 | end
95 | end
96 |
97 | def create_or_assign_record(key, attributes)
98 | i = key.to_i
99 |
100 | if dynamic_key?(i)
101 | create_record(attributes)
102 | else
103 | if call_reject_if(attributes)
104 | forms[i].delete
105 | end
106 | forms[i].submit(attributes)
107 | end
108 | end
109 |
110 | def has_destroy_flag?(attributes)
111 | attributes['_destroy'] == "1"
112 | end
113 |
114 | def assign_forms
115 | if parent.persisted?
116 | fetch_models
117 | else
118 | initialize_models
119 | end
120 | end
121 |
122 | def dynamic_key?(i)
123 | i > forms.size
124 | end
125 |
126 | def aggregate_form_errors
127 | forms.each do |form|
128 | form.valid?
129 | collect_errors_from(form)
130 | end
131 | end
132 |
133 | def fetch_models
134 | associated_records = parent.send(association_name)
135 |
136 | associated_records.each do |model|
137 | form = Form.new(association_name, parent, proc, model)
138 | forms << form
139 | end
140 | end
141 |
142 | def initialize_models
143 | records.times do
144 | form = Form.new(association_name, parent, proc)
145 | forms << form
146 | end
147 | end
148 |
149 | def collect_errors_from(model)
150 | model.errors.each do |attribute, error|
151 | errors.add(attribute, error)
152 | end
153 | end
154 |
155 | def check_record_limit!(limit, attributes_collection)
156 | if attributes_collection.size > limit
157 | raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
158 | end
159 | end
160 |
161 | def find_form_by_model_id(id)
162 | forms.select { |form| form.id == id.to_i }.first
163 | end
164 |
165 | def remove_form(form)
166 | forms.delete(form)
167 | end
168 |
169 | def create_form
170 | new_form = Form.new(association_name, parent, proc)
171 | forms << new_form
172 | new_form
173 | end
174 | end
175 |
176 | end
177 |
--------------------------------------------------------------------------------
/test/forms/single_model_form_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require_relative '../fixtures/user_form_fixture'
3 |
4 | class SingleModelFormTest < ActiveSupport::TestCase
5 | include ActiveModel::Lint::Tests
6 | fixtures :users
7 |
8 | def setup
9 | @user = User.new
10 | @form = UserFormFixture.new(@user)
11 | @model = @form
12 | end
13 |
14 | test "accepts the model it represents" do
15 | assert_equal @user, @form.model
16 | end
17 |
18 | test "declares form attributes" do
19 | attributes = [:name, :name=, :age, :age=, :gender, :gender=]
20 |
21 | attributes.each do |attribute|
22 | assert_respond_to @form, attribute
23 | end
24 | end
25 |
26 | test "delegates attributes to the model" do
27 | @form.name = "Peter"
28 | @form.age = 23
29 | @form.gender = 0
30 |
31 | assert_equal "Peter", @user.name
32 | assert_equal 23, @user.age
33 | assert_equal 0, @user.gender
34 | end
35 |
36 | test "validates itself" do
37 | @form.name = nil
38 | @form.age = nil
39 | @form.gender = nil
40 |
41 | assert_not @form.valid?
42 | [:name, :age, :gender].each do |attribute|
43 | assert_includes @form.errors.messages[attribute], "can't be blank"
44 | end
45 |
46 | @form.name = "Peters"
47 | @form.age = 23
48 | @form.gender = 0
49 |
50 | assert @form.valid?
51 | end
52 |
53 | test "validates the model" do
54 | peter = users(:peter)
55 | @form.name = peter.name
56 | @form.age = 23
57 | @form.gender = 0
58 |
59 | assert_not @form.valid?
60 | assert_includes @form.errors.messages[:name], "has already been taken"
61 | end
62 |
63 | test "sync the model with submitted data" do
64 | params = {
65 | name: "Peters",
66 | age: "23",
67 | gender: "0"
68 | }
69 |
70 | @form.submit(params)
71 |
72 | assert_equal "Peters", @form.name
73 | assert_equal 23, @form.age
74 | assert_equal 0, @form.gender
75 | end
76 |
77 | test "sync the form with existing model" do
78 | peter = users(:peter)
79 | form = UserFormFixture.new(peter)
80 |
81 | assert_equal "m-peter", form.name
82 | assert_equal 23, form.age
83 | assert_equal 0, form.gender
84 | end
85 |
86 | test "saves the model" do
87 | params = {
88 | name: "Peters",
89 | age: "23",
90 | gender: "0"
91 | }
92 |
93 | @form.submit(params)
94 |
95 | assert_difference('User.count') do
96 | @form.save
97 | end
98 |
99 | assert_equal "Peters", @form.name
100 | assert_equal 23, @form.age
101 | assert_equal 0, @form.gender
102 | end
103 |
104 | test "does not save the model with invalid data" do
105 | peter = users(:peter)
106 | params = {
107 | name: peter.name,
108 | age: "23",
109 | gender: nil
110 | }
111 |
112 | @form.submit(params)
113 |
114 | assert_difference('User.count', 0) do
115 | @form.save
116 | end
117 |
118 | assert_not @form.valid?
119 | assert_includes @form.errors.messages[:name], "has already been taken"
120 | assert_includes @form.errors.messages[:gender], "can't be blank"
121 | end
122 |
123 | test "updates the model" do
124 | peter = users(:peter)
125 | form = UserFormFixture.new(peter)
126 | params = {
127 | name: "Petrakos",
128 | age: peter.age,
129 | gender: peter.gender
130 | }
131 |
132 | form.submit(params)
133 |
134 | assert_difference('User.count', 0) do
135 | form.save
136 | end
137 |
138 | assert_equal "Petrakos", form.name
139 | end
140 |
141 | test "responds to #persisted?" do
142 | assert_respond_to @form, :persisted?
143 | assert_not @form.persisted?
144 |
145 | assert save_user
146 | assert @form.persisted?
147 | end
148 |
149 | test "responds to #to_key" do
150 | assert_respond_to @form, :to_key
151 | assert_nil @form.to_key
152 |
153 | assert save_user
154 | assert_equal @user.to_key, @form.to_key
155 | end
156 |
157 | test "responds to #to_param" do
158 | assert_respond_to @form, :to_param
159 | assert_nil @form.to_param
160 |
161 | assert save_user
162 | assert_equal @user.to_param, @form.to_param
163 | end
164 |
165 | test "responds to #to_partial_path" do
166 | assert_respond_to @form, :to_partial_path
167 | assert_instance_of String, @form.to_partial_path
168 | end
169 |
170 | test "responds to #to_model" do
171 | assert_respond_to @form, :to_model
172 | assert_equal @user, @form.to_model
173 | end
174 |
175 | private
176 |
177 | def save_user
178 | @form.name = "Peters"
179 | @form.age = 23
180 | @form.gender = 0
181 |
182 | @form.save
183 | end
184 | end
--------------------------------------------------------------------------------
/test/dummy/config/initializers/simple_form.rb:
--------------------------------------------------------------------------------
1 | # Use this setup block to configure all options available in SimpleForm.
2 | SimpleForm.setup do |config|
3 | # Wrappers are used by the form builder to generate a complete input.
4 | # You can remove any component from the wrapper, change the order or even
5 | # add your own to the stack. The options given to the wrappers method
6 | # are used to wrap the whole input (if any exists).
7 |
8 | config.wrappers :inline, class: 'clearfix', error_class: :error do |b|
9 | b.use :placeholder
10 | b.use :label
11 |
12 | b.wrapper tag: "div", class: "input" do |ba|
13 | ba.use :input
14 | ba.use :error, wrap_with: { tag: 'span', class: 'help-inline' }
15 | ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
16 | end
17 | end
18 |
19 | config.wrappers :stacked, class: "clearfix", error_class: :error do |b|
20 | b.use :placeholder
21 | b.use :label
22 | b.use :hint, tag: :span, class: :'help-block'
23 |
24 | b.wrapper tag: "div", class: "input" do |input|
25 | input.use :input
26 | input.use :error, wrap_with: { tag: :span, class: 'help-inline' }
27 | end
28 | end
29 |
30 | config.wrappers :prepend, class: "clearfix", error_class: :error do |b|
31 | b.use :placeholder
32 | b.use :label
33 | b.use :hint, tag: :span, class: :'help-block'
34 |
35 | b.wrapper tag: "div", class: "input" do |input|
36 | input.wrapper tag: "div", class: "input-prepend" do |prepend|
37 | prepend.use :input
38 | end
39 | input.use :error, wrap_with: { tag: :span, class: 'help-inline' }
40 | end
41 | end
42 |
43 | config.wrappers :append, class: "clearfix", error_class: :error do |b|
44 | b.use :placeholder
45 | b.use :label
46 | b.use :hint, tag: :span, class: :'help-block'
47 |
48 | b.wrapper tag: "div", class: "input" do |input|
49 | input.wrapper tag: "div", class: "input-append" do |append|
50 | append.use :input
51 | end
52 | input.use :error, wrap_with: { tag: :span, class: 'help-inline' }
53 | end
54 | end
55 |
56 | # Method used to tidy up errors.
57 | # config.error_method = :first
58 |
59 | # Default tag used for error notification helper.
60 | # config.error_notification_tag = :p
61 |
62 | # CSS class to add for error notification helper.
63 | # config.error_notification_class = :error_notification
64 |
65 | # ID to add for error notification helper.
66 | # config.error_notification_id = nil
67 |
68 | # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none.
69 | # config.collection_wrapper_tag = nil
70 |
71 | # You can wrap each item in a collection of radio/check boxes with a tag, defaulting to span.
72 | # config.item_wrapper_tag = :span
73 |
74 | # Series of attempts to detect a default label method for collection.
75 | # config.collection_label_methods = [ :to_label, :name, :title, :to_s ]
76 |
77 | # Series of attempts to detect a default value method for collection.
78 | # config.collection_value_methods = [ :id, :to_s ]
79 |
80 | # How the label text should be generated altogether with the required text.
81 | config.label_text = lambda { |label, required| "#{label} #{required}" }
82 |
83 | # You can define the class to use on all labels. Default is nil.
84 | # config.label_class = nil
85 |
86 | # You can define the class to use on all forms. Default is simple_form.
87 | # config.form_class = :simple_form
88 |
89 | # Whether attributes are required by default (or not). Default is true.
90 | # config.required_by_default = true
91 |
92 | # Tell browsers whether to use default HTML5 validations (novalidate option).
93 | # Default is enabled.
94 | config.browser_validations = false
95 |
96 | # Determines whether HTML5 types (:email, :url, :search, :tel) and attributes
97 | # (e.g. required) are used or not. True by default.
98 | # Having this on in non-HTML5 compliant sites can cause odd behavior in
99 | # HTML5-aware browsers such as Chrome.
100 | # config.html5 = true
101 |
102 | # Custom mappings for input types. This should be a hash containing a regexp
103 | # to match as key, and the input type that will be used when the field name
104 | # matches the regexp as value.
105 | # config.input_mappings = { /count/ => :integer }
106 |
107 | # Collection of methods to detect if a file type was given.
108 | # config.file_methods = [ :mounted_as, :file?, :public_filename ]
109 |
110 | # Default priority for time_zone inputs.
111 | # config.time_zone_priority = nil
112 |
113 | # Default priority for country inputs.
114 | # config.country_priority = nil
115 |
116 | # Default size for text inputs.
117 | # config.default_input_size = 50
118 |
119 | # When false, do not use translations for labels, hints or placeholders.
120 | # config.translate = true
121 |
122 | # Default class for buttons
123 | config.button_class = 'btn'
124 | end
--------------------------------------------------------------------------------
/test/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 20140821074917) do
15 |
16 | create_table "answers", force: true do |t|
17 | t.text "content"
18 | t.integer "question_id"
19 | t.datetime "created_at"
20 | t.datetime "updated_at"
21 | end
22 |
23 | add_index "answers", ["question_id"], name: "index_answers_on_question_id"
24 |
25 | create_table "artists", force: true do |t|
26 | t.string "name"
27 | t.integer "song_id"
28 | t.datetime "created_at"
29 | t.datetime "updated_at"
30 | end
31 |
32 | add_index "artists", ["song_id"], name: "index_artists_on_song_id"
33 |
34 | create_table "assignments", force: true do |t|
35 | t.string "name"
36 | t.datetime "created_at"
37 | t.datetime "updated_at"
38 | end
39 |
40 | create_table "conferences", force: true do |t|
41 | t.string "name"
42 | t.string "city"
43 | t.datetime "created_at"
44 | t.datetime "updated_at"
45 | end
46 |
47 | create_table "emails", force: true do |t|
48 | t.string "address"
49 | t.integer "user_id"
50 | t.datetime "created_at"
51 | t.datetime "updated_at"
52 | end
53 |
54 | add_index "emails", ["user_id"], name: "index_emails_on_user_id"
55 |
56 | create_table "people", force: true do |t|
57 | t.string "name"
58 | t.string "role"
59 | t.string "description"
60 | t.integer "project_id"
61 | t.datetime "created_at"
62 | t.datetime "updated_at"
63 | end
64 |
65 | add_index "people", ["project_id"], name: "index_people_on_project_id"
66 |
67 | create_table "presentations", force: true do |t|
68 | t.string "topic"
69 | t.string "duration"
70 | t.integer "speaker_id"
71 | t.datetime "created_at"
72 | t.datetime "updated_at"
73 | end
74 |
75 | add_index "presentations", ["speaker_id"], name: "index_presentations_on_speaker_id"
76 |
77 | create_table "producers", force: true do |t|
78 | t.string "name"
79 | t.string "studio"
80 | t.integer "artist_id"
81 | t.datetime "created_at"
82 | t.datetime "updated_at"
83 | end
84 |
85 | add_index "producers", ["artist_id"], name: "index_producers_on_artist_id"
86 |
87 | create_table "profiles", force: true do |t|
88 | t.string "twitter_name"
89 | t.string "github_name"
90 | t.integer "user_id"
91 | t.datetime "created_at"
92 | t.datetime "updated_at"
93 | end
94 |
95 | add_index "profiles", ["user_id"], name: "index_profiles_on_user_id"
96 |
97 | create_table "project_tags", force: true do |t|
98 | t.integer "project_id"
99 | t.integer "tag_id"
100 | t.datetime "created_at"
101 | t.datetime "updated_at"
102 | end
103 |
104 | add_index "project_tags", ["project_id"], name: "index_project_tags_on_project_id"
105 | add_index "project_tags", ["tag_id"], name: "index_project_tags_on_tag_id"
106 |
107 | create_table "projects", force: true do |t|
108 | t.string "name"
109 | t.string "description"
110 | t.datetime "created_at"
111 | t.datetime "updated_at"
112 | t.integer "owner_id"
113 | end
114 |
115 | create_table "questions", force: true do |t|
116 | t.text "content"
117 | t.integer "survey_id"
118 | t.datetime "created_at"
119 | t.datetime "updated_at"
120 | end
121 |
122 | add_index "questions", ["survey_id"], name: "index_questions_on_survey_id"
123 |
124 | create_table "songs", force: true do |t|
125 | t.string "title"
126 | t.string "length"
127 | t.datetime "created_at"
128 | t.datetime "updated_at"
129 | end
130 |
131 | create_table "speakers", force: true do |t|
132 | t.string "name"
133 | t.string "occupation"
134 | t.integer "conference_id"
135 | t.datetime "created_at"
136 | t.datetime "updated_at"
137 | end
138 |
139 | add_index "speakers", ["conference_id"], name: "index_speakers_on_conference_id"
140 |
141 | create_table "sub_tasks", force: true do |t|
142 | t.string "name"
143 | t.string "description"
144 | t.boolean "done"
145 | t.integer "task_id"
146 | t.datetime "created_at"
147 | t.datetime "updated_at"
148 | end
149 |
150 | add_index "sub_tasks", ["task_id"], name: "index_sub_tasks_on_task_id"
151 |
152 | create_table "surveys", force: true do |t|
153 | t.string "name"
154 | t.datetime "created_at"
155 | t.datetime "updated_at"
156 | end
157 |
158 | create_table "tags", force: true do |t|
159 | t.string "name"
160 | t.datetime "created_at"
161 | t.datetime "updated_at"
162 | end
163 |
164 | create_table "tasks", force: true do |t|
165 | t.string "name"
166 | t.string "description"
167 | t.boolean "done"
168 | t.integer "project_id"
169 | t.datetime "created_at"
170 | t.datetime "updated_at"
171 | t.integer "assignment_id"
172 | end
173 |
174 | add_index "tasks", ["assignment_id"], name: "index_tasks_on_assignment_id"
175 | add_index "tasks", ["project_id"], name: "index_tasks_on_project_id"
176 |
177 | create_table "users", force: true do |t|
178 | t.string "name"
179 | t.integer "age"
180 | t.integer "gender"
181 | t.datetime "created_at"
182 | t.datetime "updated_at"
183 | end
184 |
185 | end
186 |
--------------------------------------------------------------------------------
/test/forms/nested_model_form_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require_relative '../fixtures/user_with_email_form_fixture'
3 |
4 | class NestedModelFormTest < ActiveSupport::TestCase
5 | include ActiveModel::Lint::Tests
6 | fixtures :users, :emails
7 |
8 | def setup
9 | @user = User.new
10 | @form = UserWithEmailFormFixture.new(@user)
11 | @email_form = @form.email
12 | @model = @form
13 | end
14 |
15 | test "declares association" do
16 | assert_respond_to UserWithEmailFormFixture, :association
17 | end
18 |
19 | test "contains a list of sub-forms" do
20 | assert_respond_to UserWithEmailFormFixture, :forms
21 | end
22 |
23 | test "forms list contains form definitions" do
24 | email_definition = UserWithEmailFormFixture.forms.first
25 |
26 | assert_equal :email, email_definition.assoc_name
27 | end
28 |
29 | test "contains getter for email sub-form" do
30 | assert_respond_to @form, :email
31 | assert_instance_of ActionForm::Form, @form.email
32 | end
33 |
34 | test "email sub-form contains association name and parent model" do
35 | assert_equal :email, @email_form.association_name
36 | assert_equal @user, @email_form.parent
37 | end
38 |
39 | test "email sub-form initializes model for new parent" do
40 | assert_instance_of Email, @email_form.model
41 | assert_equal @form.model.email, @email_form.model
42 | assert @email_form.model.new_record?
43 | end
44 |
45 | test "email sub-form fetches model for existing parent" do
46 | user = users(:peter)
47 | user_form = UserWithEmailFormFixture.new(user)
48 | email_form = user_form.email
49 |
50 | assert_instance_of Email, email_form.model
51 | assert_equal user_form.model.email, email_form.model
52 | assert email_form.persisted?
53 |
54 | assert_equal "m-peter", user_form.name
55 | assert_equal 23, user_form.age
56 | assert_equal 0, user_form.gender
57 | assert_equal "markoupetr@gmail.com", email_form.address
58 | end
59 |
60 | test "#represents? returns true if the argument matches the Form's association name, false otherwise" do
61 | assert @email_form.represents?("email")
62 | assert_not @email_form.represents?("profile")
63 | end
64 |
65 | test "email sub-form declares attributes" do
66 | [:address, :address=].each do |attribute|
67 | assert_respond_to @email_form, attribute
68 | end
69 | end
70 |
71 | test "email sub-form delegates attributes to model" do
72 | @email_form.address = "petrakos@gmail.com"
73 |
74 | assert_equal "petrakos@gmail.com", @email_form.address
75 | assert_equal "petrakos@gmail.com", @email_form.model.address
76 | end
77 |
78 | test "email sub-form validates itself" do
79 | @email_form.address = nil
80 |
81 | assert_not @email_form.valid?
82 | assert_includes @email_form.errors.messages[:address], "can't be blank"
83 |
84 | @email_form.address = "petrakos@gmail.com"
85 |
86 | assert @email_form.valid?
87 | end
88 |
89 | test "email sub-form validates the model" do
90 | existing_email = emails(:peters)
91 | @email_form.address = existing_email.address
92 |
93 | assert_not @email_form.valid?
94 | assert_includes @email_form.errors.messages[:address], "has already been taken"
95 |
96 | @email_form.address = "petrakos@gmail.com"
97 |
98 | assert @email_form.valid?
99 | end
100 |
101 | test "main form syncs its model and the models in nested sub-forms" do
102 | params = {
103 | name: "Petrakos",
104 | age: "23",
105 | gender: "0",
106 |
107 | email_attributes: {
108 | address: "petrakos@gmail.com"
109 | }
110 | }
111 |
112 | @form.submit(params)
113 |
114 | assert_equal "Petrakos", @form.name
115 | assert_equal 23, @form.age
116 | assert_equal 0, @form.gender
117 | assert_equal "petrakos@gmail.com", @email_form.address
118 | end
119 |
120 | test "main form saves its model and the models in nested sub-forms" do
121 | params = {
122 | name: "Petrakos",
123 | age: "23",
124 | gender: "0",
125 |
126 | email_attributes: {
127 | address: "petrakos@gmail.com"
128 | }
129 | }
130 |
131 | @form.submit(params)
132 |
133 | assert_difference(['User.count', 'Email.count']) do
134 | @form.save
135 | end
136 |
137 | assert_equal "Petrakos", @form.name
138 | assert_equal 23, @form.age
139 | assert_equal 0, @form.gender
140 | assert_equal "petrakos@gmail.com", @email_form.address
141 |
142 | assert @form.persisted?
143 | assert @email_form.persisted?
144 | end
145 |
146 | test "main form updates its model and the models in nested sub-forms" do
147 | user = users(:peter)
148 | form = UserWithEmailFormFixture.new(user)
149 | params = {
150 | name: "Petrakos",
151 | age: 24,
152 | gender: 0,
153 |
154 | email_attributes: {
155 | address: "cs3199@teilar.gr"
156 | }
157 | }
158 |
159 | form.submit(params)
160 |
161 | assert_difference(['User.count', 'Email.count'], 0) do
162 | form.save
163 | end
164 |
165 | assert_equal "Petrakos", form.name
166 | assert_equal 24, form.age
167 | assert_equal 0, form.gender
168 | assert_equal "cs3199@teilar.gr", form.email.address
169 | end
170 |
171 | test "main form collects all the model related errors" do
172 | peter = users(:peter)
173 | params = {
174 | name: peter.name,
175 | age: "23",
176 | gender: "0",
177 |
178 | email_attributes: {
179 | address: peter.email.address
180 | }
181 | }
182 |
183 | @form.submit(params)
184 |
185 | assert_difference(['User.count', 'Email.count'], 0) do
186 | @form.save
187 | end
188 |
189 | assert_includes @form.errors[:name], "has already been taken"
190 | assert_includes @form.errors["email.address"], "has already been taken"
191 | end
192 |
193 | test "main form collects all the form specific errors" do
194 | params = {
195 | name: nil,
196 | age: nil,
197 | gender: nil,
198 |
199 | email_attributes: {
200 | address: nil
201 | }
202 | }
203 |
204 | @form.submit(params)
205 |
206 | assert_not @form.valid?
207 |
208 | assert_includes @form.errors[:name], "can't be blank"
209 | assert_includes @form.errors[:age], "can't be blank"
210 | assert_includes @form.errors[:gender], "can't be blank"
211 | assert_includes @form.errors["email.address"], "can't be blank"
212 | end
213 |
214 | test "main form responds to writer method" do
215 | assert_respond_to @form, :email_attributes=
216 | end
217 | end
218 |
--------------------------------------------------------------------------------
/test/forms/two_nesting_level_form_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TwoNestingLevelFormTest < ActiveSupport::TestCase
4 | include ActiveModel::Lint::Tests
5 | fixtures :songs, :artists, :producers
6 |
7 | def setup
8 | @song = Song.new
9 | @form = SongForm.new(@song)
10 | @producer_form = @form.artist.producer
11 | @model = @form
12 | end
13 |
14 | test "contains getter for producer sub-form" do
15 | assert_respond_to @form.artist, :producer
16 | assert_instance_of ActionForm::Form, @producer_form
17 | end
18 |
19 | test "producer sub-form contains association name and parent model" do
20 | assert_equal :producer, @producer_form.association_name
21 | assert_instance_of Producer, @producer_form.model
22 | assert_instance_of Artist, @producer_form.parent
23 | end
24 |
25 | test "producer sub-form initializes models for new parent" do
26 | assert_equal @form.artist.model.producer, @producer_form.model
27 | assert @producer_form.model.new_record?
28 | end
29 |
30 | test "producer sub-form fetches models for existing parent" do
31 | song = songs(:lockdown)
32 | form = SongForm.new(song)
33 | artist_form = form.artist
34 | producer_form = artist_form.producer
35 |
36 | assert_equal "Love Lockdown", form.title
37 | assert_equal "350", form.length
38 | assert form.persisted?
39 |
40 | assert_equal "Kanye West", artist_form.name
41 | assert artist_form.persisted?
42 |
43 | assert_equal "Jay-Z", producer_form.name
44 | assert_equal "Ztudio", producer_form.studio
45 | assert producer_form.persisted?
46 | end
47 |
48 | test "producer sub-form declares attributes" do
49 | attributes = [:name, :name=, :studio, :studio=]
50 |
51 | attributes.each do |attribute|
52 | assert_respond_to @producer_form, attribute
53 | end
54 | end
55 |
56 | test "producer sub-form delegates attributes to model" do
57 | @producer_form.name = "Phoebos"
58 | @producer_form.studio = "MADog"
59 |
60 | assert_equal "Phoebos", @producer_form.name
61 | assert_equal "MADog", @producer_form.studio
62 |
63 | assert_equal "Phoebos", @producer_form.model.name
64 | assert_equal "MADog", @producer_form.model.studio
65 | end
66 |
67 | test "main form syncs its model and the models in nested sub-forms" do
68 | params = {
69 | title: "Diamonds",
70 | length: "360",
71 |
72 | artist_attributes: {
73 | name: "Karras",
74 |
75 | producer_attributes: {
76 | name: "Phoebos",
77 | studio: "MADog"
78 | }
79 | }
80 | }
81 |
82 | @form.submit(params)
83 |
84 | assert_equal "Diamonds", @form.title
85 | assert_equal "360", @form.length
86 | assert_equal "Karras", @form.artist.name
87 | assert_equal "Phoebos", @producer_form.name
88 | assert_equal "MADog", @producer_form.studio
89 | end
90 |
91 | test "main form validates itself" do
92 | params = {
93 | title: nil,
94 | length: nil,
95 |
96 | artist_attributes: {
97 | name: nil,
98 |
99 | producer_attributes: {
100 | name: nil,
101 | studio: nil
102 | }
103 | }
104 | }
105 |
106 | @form.submit(params)
107 |
108 | assert_not @form.valid?
109 | assert_includes @form.errors[:title], "can't be blank"
110 | assert_includes @form.errors[:length], "can't be blank"
111 | assert_includes @form.errors["artist.name"], "can't be blank"
112 | assert_includes @form.errors["artist.producer.studio"], "can't be blank"
113 |
114 | @form.title = "Diamonds"
115 | @form.length = "355"
116 | @form.artist.name = "Karras"
117 | @producer_form.name = "Phoebos"
118 | @producer_form.studio = "MADog"
119 |
120 | assert @form.valid?
121 | end
122 |
123 | test "main form validates the models" do
124 | song = songs(:lockdown)
125 | params = {
126 | title: song.title,
127 | length: nil,
128 |
129 | artist_attributes: {
130 | name: song.artist.name,
131 |
132 | producer_attributes: {
133 | name: song.artist.producer.name,
134 | studio: song.artist.producer.studio
135 | }
136 | }
137 | }
138 |
139 | @form.submit(params)
140 |
141 | assert_not @form.valid?
142 | assert_includes @form.errors[:title], "has already been taken"
143 | assert_includes @form.errors["artist.name"], "has already been taken"
144 | assert_includes @form.errors["artist.producer.name"], "has already been taken"
145 | assert_includes @form.errors["artist.producer.studio"], "has already been taken"
146 | end
147 |
148 | test "main form saves its model and the models in nested sub-forms" do
149 | params = {
150 | title: "Diamonds",
151 | length: "360",
152 |
153 | artist_attributes: {
154 | name: "Karras",
155 |
156 | producer_attributes: {
157 | name: "Phoebos",
158 | studio: "MADog"
159 | }
160 | }
161 | }
162 |
163 | @form.submit(params)
164 |
165 | assert_difference(['Song.count', 'Artist.count', 'Producer.count']) do
166 | @form.save
167 | end
168 |
169 | assert_equal "Diamonds", @form.title
170 | assert_equal "360", @form.length
171 | assert_equal "Karras", @form.artist.name
172 | assert_equal "Phoebos", @producer_form.name
173 | assert_equal "MADog", @producer_form.studio
174 |
175 | assert @form.persisted?
176 | assert @form.artist.persisted?
177 | assert @producer_form.persisted?
178 | end
179 |
180 | test "main form updates its model and the models in nested sub-forms" do
181 | song = songs(:lockdown)
182 | params = {
183 | title: "Diamonds",
184 | length: "360",
185 |
186 | artist_attributes: {
187 | name: "Karras",
188 |
189 | producer_attributes: {
190 | name: "Phoebos",
191 | studio: "MADog"
192 | }
193 | }
194 | }
195 | form = SongForm.new(song)
196 |
197 | form.submit(params)
198 |
199 | assert_difference(['Song.count', 'Artist.count', 'Producer.count'], 0) do
200 | form.save
201 | end
202 |
203 | assert_equal "Diamonds", form.title
204 | assert_equal "360", form.length
205 | assert_equal "Karras", form.artist.name
206 | assert_equal "Phoebos", form.artist.producer.name
207 | assert_equal "MADog", form.artist.producer.studio
208 |
209 | assert form.persisted?
210 | assert form.artist.persisted?
211 | assert form.artist.producer.persisted?
212 | end
213 |
214 | test "main form responds to writer method" do
215 | assert_respond_to @form, :artist_attributes=
216 | assert_respond_to @form.artist, :producer_attributes=
217 | end
218 | end
219 |
--------------------------------------------------------------------------------
/test/forms/nested_models_form_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class NestedModelsFormTest < ActiveSupport::TestCase
4 | include ActiveModel::Lint::Tests
5 | fixtures :users, :emails, :profiles
6 |
7 | def setup
8 | @user = User.new
9 | @form = UserForm.new(@user)
10 | @profile_form = @form.profile
11 | @model = @form
12 | end
13 |
14 | test "declares both sub-forms" do
15 | assert_equal 2, UserForm.forms.size
16 | assert_equal 2, @form.forms.size
17 | end
18 |
19 | test "forms list contains profile sub-form definition" do
20 | profile_definition = UserForm.forms.last
21 |
22 | assert_equal :profile, profile_definition.assoc_name
23 | end
24 |
25 | test "profile sub-form contains association name and parent" do
26 | assert_equal :profile, @profile_form.association_name
27 | assert_equal @user, @profile_form.parent
28 | end
29 |
30 | test "profile sub-form declares attributes" do
31 | attributes = [:twitter_name, :twitter_name=, :github_name, :github_name=]
32 |
33 | attributes.each do |attribute|
34 | assert_respond_to @profile_form, attribute
35 | end
36 | end
37 |
38 | test "profile sub-form delegates attributes to model" do
39 | @profile_form.twitter_name = "twitter_peter"
40 | @profile_form.github_name = "github_peter"
41 |
42 | assert_equal "twitter_peter", @profile_form.twitter_name
43 | assert_equal "twitter_peter", @profile_form.model.twitter_name
44 |
45 | assert_equal "github_peter", @profile_form.github_name
46 | assert_equal "github_peter", @profile_form.model.github_name
47 | end
48 |
49 | test "profile sub-form initializes model for new parent" do
50 | assert_instance_of Profile, @profile_form.model
51 | assert_equal @form.model.profile, @profile_form.model
52 | assert @profile_form.model.new_record?
53 | end
54 |
55 | test "profile sub-form fetches model for existing parent" do
56 | user = users(:peter)
57 | user_form = UserForm.new(user)
58 | profile_form = user_form.profile
59 |
60 | assert_instance_of Profile, profile_form.model
61 | assert_equal user_form.model.profile, profile_form.model
62 | assert profile_form.persisted?
63 |
64 | assert_equal "m-peter", user_form.name
65 | assert_equal 23, user_form.age
66 | assert_equal 0, user_form.gender
67 | assert_equal "twitter_peter", profile_form.model.twitter_name
68 | assert_equal "github_peter", profile_form.model.github_name
69 | end
70 |
71 | test "profile sub-form validates itself" do
72 | @profile_form.twitter_name = nil
73 | @profile_form.github_name = nil
74 |
75 | assert_not @profile_form.valid?
76 | [:twitter_name, :github_name].each do |attribute|
77 | assert_includes @profile_form.errors.messages[attribute], "can't be blank"
78 | end
79 |
80 | @profile_form.twitter_name = "t-peter"
81 | @profile_form.github_name = "g-peter"
82 |
83 | assert @profile_form.valid?
84 | end
85 |
86 | test "main form syncs its model and the models in nested sub-forms" do
87 | params = {
88 | name: "Petrakos",
89 | age: "23",
90 | gender: "0",
91 |
92 | email_attributes: {
93 | address: "petrakos@gmail.com"
94 | },
95 |
96 | profile_attributes: {
97 | twitter_name: "t_peter",
98 | github_name: "g_peter"
99 | }
100 | }
101 |
102 | @form.submit(params)
103 |
104 | assert_equal "Petrakos", @form.name
105 | assert_equal 23, @form.age
106 | assert_equal 0, @form.gender
107 | assert_equal "petrakos@gmail.com", @form.email.address
108 | assert_equal "t_peter", @profile_form.twitter_name
109 | assert_equal "g_peter", @profile_form.github_name
110 | end
111 |
112 | test "main form saves its model and the models in nested sub-forms" do
113 | params = {
114 | name: "Petrakos",
115 | age: "23",
116 | gender: "0",
117 |
118 | email_attributes: {
119 | address: "petrakos@gmail.com"
120 | },
121 |
122 | profile_attributes: {
123 | twitter_name: "t_peter",
124 | github_name: "g_peter"
125 | }
126 | }
127 |
128 | @form.submit(params)
129 |
130 | assert_difference(['User.count', 'Email.count', 'Profile.count']) do
131 | @form.save
132 | end
133 |
134 | assert_equal "Petrakos", @form.name
135 | assert_equal 23, @form.age
136 | assert_equal 0, @form.gender
137 | assert_equal "petrakos@gmail.com", @form.email.address
138 | assert_equal "t_peter", @profile_form.twitter_name
139 | assert_equal "g_peter", @profile_form.github_name
140 |
141 | assert @form.persisted?
142 | assert @form.email.persisted?
143 | assert @profile_form.persisted?
144 | end
145 |
146 | test "main form updates its model and the models in nested sub-forms" do
147 | user = users(:peter)
148 | form = UserForm.new(user)
149 | params = {
150 | name: "Petrakos",
151 | age: 24,
152 | gender: 0,
153 |
154 | email_attributes: {
155 | address: "cs3199@teilar.gr"
156 | },
157 |
158 | profile_attributes: {
159 | twitter_name: "peter_t",
160 | github_name: "peter_g"
161 | }
162 | }
163 |
164 | form.submit(params)
165 |
166 | assert_difference(['User.count', 'Email.count'], 0) do
167 | form.save
168 | end
169 |
170 | assert_equal "Petrakos", form.name
171 | assert_equal 24, form.age
172 | assert_equal 0, form.gender
173 | assert_equal "cs3199@teilar.gr", form.email.address
174 | assert_equal "peter_t", form.profile.twitter_name
175 | assert_equal "peter_g", form.profile.github_name
176 | end
177 |
178 | test "main form collects all the model related errors" do
179 | peter = users(:peter)
180 | params = {
181 | name: peter.name,
182 | age: "23",
183 | gender: "0",
184 |
185 | email_attributes: {
186 | address: peter.email.address
187 | },
188 |
189 | profile_attributes: {
190 | twitter_name: peter.profile.twitter_name,
191 | github_name: peter.profile.github_name
192 | }
193 | }
194 |
195 | @form.submit(params)
196 |
197 | assert_difference(['User.count', 'Email.count', 'Profile.count'], 0) do
198 | @form.save
199 | end
200 |
201 | assert_includes @form.errors[:name], "has already been taken"
202 | assert_includes @form.errors["email.address"], "has already been taken"
203 | assert_includes @form.errors["profile.twitter_name"], "has already been taken"
204 | assert_includes @form.errors["profile.github_name"], "has already been taken"
205 | end
206 |
207 | test "main form collects all the form specific errors" do
208 | params = {
209 | name: nil,
210 | age: nil,
211 | gender: nil,
212 |
213 | email_attributes: {
214 | address: nil
215 | },
216 |
217 | profile_attributes: {
218 | twitter_name: nil,
219 | github_name: nil
220 | }
221 | }
222 |
223 | @form.submit(params)
224 |
225 | assert_not @form.valid?
226 |
227 | assert_includes @form.errors[:name], "can't be blank"
228 | assert_includes @form.errors[:age], "can't be blank"
229 | assert_includes @form.errors[:gender], "can't be blank"
230 | assert_includes @form.errors["email.address"], "can't be blank"
231 | assert_includes @form.errors["profile.twitter_name"], "can't be blank"
232 | assert_includes @form.errors["profile.github_name"], "can't be blank"
233 | end
234 |
235 | test "main form responds to writer method" do
236 | assert_respond_to @form, :email_attributes=
237 | assert_respond_to @form, :profile_attributes=
238 | end
239 | end
240 |
--------------------------------------------------------------------------------
/test/dummy/test/controllers/conferences_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ConferencesControllerTest < ActionController::TestCase
4 | fixtures :conferences, :speakers, :presentations
5 |
6 | setup do
7 | @conference = conferences(:ruby)
8 | end
9 |
10 | test "should get index" do
11 | get :index
12 | assert_response :success
13 | assert_not_nil assigns(:conferences)
14 | end
15 |
16 | test "should get new" do
17 | get :new
18 | assert_response :success
19 | end
20 |
21 | test "should create conference" do
22 | assert_difference('Conference.count') do
23 | post :create, conference: {
24 | name: "Euruco",
25 | city: "Athens",
26 |
27 | speaker_attributes: {
28 | name: "Petros Markou",
29 | occupation: "Developer",
30 |
31 | presentations_attributes: {
32 | "0" => { topic: "Ruby OOP", duration: "1h" },
33 | "1" => { topic: "Ruby Closures", duration: "1h" },
34 | }
35 | }
36 | }
37 | end
38 |
39 | conference_form = assigns(:conference_form)
40 |
41 | assert conference_form.valid?
42 | assert_redirected_to conference_path(conference_form)
43 |
44 | assert_equal "Euruco", conference_form.name
45 | assert_equal "Athens", conference_form.city
46 |
47 | assert_equal "Petros Markou", conference_form.speaker.name
48 | assert_equal "Developer", conference_form.speaker.occupation
49 |
50 | assert_equal "Ruby OOP", conference_form.speaker.presentations[0].topic
51 | assert_equal "1h", conference_form.speaker.presentations[0].duration
52 | assert_equal "Ruby Closures", conference_form.speaker.presentations[1].topic
53 | assert_equal "1h", conference_form.speaker.presentations[1].duration
54 |
55 | assert conference_form.speaker.persisted?
56 | conference_form.speaker.presentations.each do |presentation|
57 | presentation.persisted?
58 | end
59 |
60 | assert_equal "Conference: #{conference_form.name} was successfully created.", flash[:notice]
61 | end
62 |
63 | test "should create dynamically added presentation to speaker" do
64 | assert_difference('Conference.count') do
65 | post :create, conference: {
66 | name: "Euruco",
67 | city: "Athens",
68 |
69 | speaker_attributes: {
70 | name: "Petros Markou",
71 | occupation: "Developer",
72 |
73 | presentations_attributes: {
74 | "0" => { topic: "Ruby OOP", duration: "1h" },
75 | "1" => { topic: "Ruby Closures", duration: "1h" },
76 | "12312" => { topic: "Ruby Metaprogramming", duration: "2h" }
77 | }
78 | }
79 | }
80 | end
81 |
82 | conference_form = assigns(:conference_form)
83 |
84 | assert conference_form.valid?
85 | assert_redirected_to conference_path(conference_form)
86 |
87 | assert_equal "Euruco", conference_form.name
88 | assert_equal "Athens", conference_form.city
89 |
90 | assert_equal "Petros Markou", conference_form.speaker.name
91 | assert_equal "Developer", conference_form.speaker.occupation
92 |
93 | assert_equal 3, conference_form.speaker.presentations.size
94 |
95 | assert_equal "Ruby OOP", conference_form.speaker.presentations[0].topic
96 | assert_equal "1h", conference_form.speaker.presentations[0].duration
97 | assert_equal "Ruby Closures", conference_form.speaker.presentations[1].topic
98 | assert_equal "1h", conference_form.speaker.presentations[1].duration
99 | assert_equal "Ruby Metaprogramming", conference_form.speaker.presentations[2].topic
100 | assert_equal "2h", conference_form.speaker.presentations[2].duration
101 |
102 | assert conference_form.speaker.persisted?
103 | conference_form.speaker.presentations.each do |presentation|
104 | presentation.persisted?
105 | end
106 |
107 | assert_equal "Conference: #{conference_form.name} was successfully created.", flash[:notice]
108 | end
109 |
110 | test "should not create conference with invalid params" do
111 | conference = conferences(:ruby)
112 |
113 | assert_difference(['Conference.count', 'Speaker.count'], 0) do
114 | post :create, conference: {
115 | name: conference.name,
116 | city: nil,
117 |
118 | speaker_attributes: {
119 | name: conference.speaker.name,
120 | occupation: "Developer",
121 |
122 | presentations_attributes: {
123 | "0" => { topic: nil, duration: "1h" },
124 | "1" => { topic: "Ruby Closures", duration: nil },
125 | }
126 | }
127 | }
128 | end
129 |
130 | conference_form = assigns(:conference_form)
131 |
132 | assert_not conference_form.valid?
133 |
134 | assert_includes conference_form.errors.messages[:name], "has already been taken"
135 | assert_includes conference_form.errors.messages[:city], "can't be blank"
136 |
137 | assert_includes conference_form.speaker.errors.messages[:name], "has already been taken"
138 |
139 | assert_includes conference_form.speaker.presentations[0].errors.messages[:topic], "can't be blank"
140 | assert_includes conference_form.speaker.presentations[1].errors.messages[:duration], "can't be blank"
141 | end
142 |
143 | test "should show conference" do
144 | get :show, id: @conference
145 | assert_response :success
146 | end
147 |
148 | test "should get edit" do
149 | get :edit, id: @conference
150 | assert_response :success
151 | end
152 |
153 | test "should update conference" do
154 | assert_difference('Conference.count', 0) do
155 | patch :update, id: @conference, conference: {
156 | name: "GoGaruco",
157 | city: "Golden State",
158 |
159 | speaker_attributes: {
160 | name: "John Doe",
161 | occupation: "Developer",
162 |
163 | presentations_attributes: {
164 | "0" => { topic: "Rails OOP", duration: "1h", id: presentations(:ruby_oop).id },
165 | "1" => { topic: "Rails Patterns", duration: "1h", id: presentations(:ruby_closures).id },
166 | }
167 | }
168 | }
169 | end
170 |
171 | conference_form = assigns(:conference_form)
172 |
173 | assert_redirected_to conference_path(conference_form)
174 |
175 | assert_equal "GoGaruco", conference_form.name
176 | assert_equal "Golden State", conference_form.city
177 |
178 | assert_equal "John Doe", conference_form.speaker.name
179 | assert_equal "Developer", conference_form.speaker.occupation
180 |
181 | assert_equal "Rails Patterns", conference_form.speaker.presentations[0].topic
182 | assert_equal "1h", conference_form.speaker.presentations[0].duration
183 | assert_equal "Rails OOP", conference_form.speaker.presentations[1].topic
184 | assert_equal "1h", conference_form.speaker.presentations[1].duration
185 |
186 | assert_equal "Conference: #{conference_form.name} was successfully updated.", flash[:notice]
187 | end
188 |
189 | test "should destroy dynamically removed presentation from speaker" do
190 | assert_difference('Conference.count', 0) do
191 | patch :update, id: @conference, conference: {
192 | name: "GoGaruco",
193 | city: "Golden State",
194 |
195 | speaker_attributes: {
196 | name: "John Doe",
197 | occupation: "Developer",
198 |
199 | presentations_attributes: {
200 | "0" => { topic: "Rails OOP", duration: "1h", id: presentations(:ruby_oop).id },
201 | "1" => { topic: "Rails Patterns", duration: "1h", id: presentations(:ruby_closures).id, _destroy: "1" },
202 | }
203 | }
204 | }
205 | end
206 |
207 | conference_form = assigns(:conference_form)
208 |
209 | assert_redirected_to conference_path(conference_form)
210 |
211 | assert_equal "GoGaruco", conference_form.name
212 | assert_equal "Golden State", conference_form.city
213 |
214 | assert_equal "John Doe", conference_form.speaker.name
215 | assert_equal "Developer", conference_form.speaker.occupation
216 |
217 | assert_equal "Rails OOP", conference_form.speaker.presentations[0].topic
218 | assert_equal "1h", conference_form.speaker.presentations[0].duration
219 |
220 | assert_equal 1, conference_form.speaker.presentations.size
221 |
222 | assert_equal "Conference: #{conference_form.name} was successfully updated.", flash[:notice]
223 | end
224 |
225 | test "should destroy conference" do
226 | assert_difference('Conference.count', -1) do
227 | delete :destroy, id: @conference
228 | end
229 |
230 | assert_redirected_to conferences_path
231 | end
232 | end
233 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Action Form
2 |
3 | # DISCLAIMER: This project is an experiment and should not be used in applications.
4 |
5 | [](https://travis-ci.org/railsgsoc/actionform)
6 |
7 | Set your models free from the `accepts_nested_attributes_for` helper. Action Form provides an object-oriented approach to represent your forms by building a form object, rather than relying on Active Record internals for doing this. Form objects provide an API to describe the models involved in the form, their attributes and validations. A form object deals with create/update actions of nested objects in a more seamless way.
8 |
9 | ## Installation
10 |
11 | Add this line to your `Gemfile`:
12 |
13 | ```ruby
14 | gem 'actionform'
15 | ```
16 |
17 | ## Defining Forms
18 |
19 | Consider an example where you want to create/update a conference that can have many speakers which can present a single presentation with one form submission. You start by defining a form to represent the root model, `Conference`:
20 |
21 | ```ruby
22 | class ConferenceForm < ActionForm::Base
23 | self.main_model = :conference
24 |
25 | attributes :name, :city
26 |
27 | validates :name, :city, presence: true
28 | end
29 | ```
30 |
31 | Your form object has to subclass `ActionForm::Base` in order to gain the necessary API. When defining the form, you have to specify the main_model the form represents with the following line:
32 |
33 | ```ruby
34 | self.main_model = :conference
35 | ```
36 |
37 | To add fields to the form, use the `attributes` or `attribute` class method. The form can also define validation rules for the model it represents. For the `presence` validation rule there is a short inline syntax:
38 |
39 | ```ruby
40 | class ConferenceForm < ActionForm::Base
41 | attributes :name, :city, required: true
42 | end
43 | ```
44 |
45 | ## The API
46 |
47 | The `ActionForm::Base` class provides a simple API with only a few instance/class methods. Below are listed the instance methods:
48 |
49 | 1. `initialize(model)` accepts an instance of the model that the form represents.
50 | 2. `submit(params)` updates the main form's model and nested models with the posted parameters. The models are not saved/updated until you call `save`.
51 | 3. `errors` returns validation messages in a classy Active Model style.
52 | 4. `save` will call `save` on the model and nested models. This method will validate the model and nested models and if no error arises then it will save them and return true.
53 |
54 | The following are the class methods:
55 |
56 | 1. `attributes` accepts the names of attributes to define on the form. If you want to declare a presence validation rule for the given attributes, you can pass in the `required: true` option as showcased above. The `attribute` method is aliased to the `attributes` method.
57 | 2. `association(name, options={}, &block)` defines a nested form for the `name` model. If the model is a `has_many` association you can pass in the `records: x` option and fields to create `x` objects will be rendered. If you pass a block, you can define another nested form the same way.
58 |
59 | In addition to the main API, forms expose accessors to the defined attributes. This is used for rendering or manual operations.
60 |
61 | ## Setup
62 |
63 | In your controller you create a form instance and pass in the model you want to work on.
64 |
65 | ```ruby
66 | class ConferencesController
67 | def new
68 | conference = Conference.new
69 | @conference_form = ConferenceForm.new(conference)
70 | end
71 | end
72 | ```
73 |
74 | You can also setup the form for editing existing items.
75 |
76 | ```ruby
77 | class ConferencesController
78 | def edit
79 | conference = Conference.find(params[:id])
80 | @conference_form = ConferenceForm.new(conference)
81 | end
82 | end
83 | ```
84 |
85 | Action Form will read property values from the model in setup. Given the following form class.
86 |
87 | ```ruby
88 | class ConferenceForm < ActionForm::Base
89 | attribute :name
90 | end
91 | ```
92 |
93 | Internally, this form will call `conference.name` to populate the name field.
94 |
95 | ## Rendering Forms
96 |
97 | Your `@conference_form` is now ready to be rendered, either do it yourself or use something like Rails' `form_for`, `simple_form` or `formtastic`.
98 |
99 | ```erb
100 | <%= form_for @conference_form do |f| %>
101 | <%= f.text_field :name %>
102 | <%= f.text_field :city %>
103 | <% end %>
104 | ```
105 |
106 | Nested forms and collections can be easily rendered with `fields_for`, etc. Just use Action Form as if it would be an Active Model instance in the view layer.
107 |
108 | ## Syncing Back
109 |
110 | After setting up your form object, you can populate the models with the submitted parameters.
111 |
112 | ```ruby
113 | class ConferencesController
114 | def create
115 | conference = Conference.new
116 | @conference_form = ConferenceForm.new(conference)
117 | @conference_form.submit(conference_params)
118 | end
119 | end
120 | ```
121 |
122 | This will write all the properties back to the model. In a nested form, this works recursively, of course.
123 |
124 | ## Saving Forms
125 |
126 | After the form is populated with the posted data, you can save the model by calling `save`.
127 |
128 | ```ruby
129 | class ConferencesController
130 | def create
131 | conference = Conference.new
132 | @conference_form = ConferenceForm.new(conference)
133 | @conference_form.submit(conference_params)
134 |
135 | if @conference_form.save
136 | redirect_to @conference_form, notice: "Conference: #{@conference_form.name} was successfully created." }
137 | else
138 | render :new
139 | end
140 | end
141 | end
142 | ```
143 |
144 | If the `save` method returns false due to validation errors defined on the form, you can render it again with the data that has been submitted and the errors found.
145 |
146 | ## Nesting Forms: 1-n Relations
147 |
148 | Action Form also gives you nested collections.
149 |
150 | Let's define the `has_many :speakers` collection association on the `Conference` model.
151 |
152 | ```ruby
153 | class Conference < ActiveRecord::Base
154 | has_many :speakers
155 | validates :name, uniqueness: true
156 | end
157 | ```
158 |
159 | The form should look like this.
160 |
161 | ```ruby
162 | class ConferenceForm < ActionForm::Base
163 | attributes :name, :city, required: true
164 |
165 | association :speakers do
166 | attributes :name, :occupation, required: true
167 | end
168 | end
169 | ```
170 |
171 | By default, the `association :speakers` declaration will create a single `Speaker` object. You can specify how many objects you want in your form to be rendered with the `new` action as follows: `association: speakers, records: 2`. This will create 2 new `Speaker` objects, and of course fields to create 2 `Speaker` objects. There are also some link helpers to dynamically add/remove objects from collection associations. Read below.
172 |
173 | This basically works like a nested `property` that iterates over a collection of speakers.
174 |
175 | ### has_many: Rendering
176 |
177 | Action Form will expose the collection using the `speakers` method.
178 |
179 | ```erb
180 | <%= form_for @conference_form |f| %>
181 | <%= f.text_field :name %>
182 | <%= f.text_field :city %>
183 |
184 | <%= f.fields_for :speakers do |s| %>
185 | <%= s.text_field :name %>
186 | <%= s.text_field :occupation %>
187 | <% end %>
188 | <% end %>
189 | ```
190 |
191 | ## Nesting Forms: 1-1 Relations
192 |
193 | Speakers are allowed to have 1 Presentation.
194 |
195 | ```ruby
196 | class Speaker < ActiveRecord::Base
197 | has_one :presentation
198 | belongs_to :conference
199 | validates :name, uniqueness: true
200 | end
201 | ```
202 |
203 | The full form should look like this:
204 |
205 | ```ruby
206 | class ConferenceForm < ActionForm::Base
207 | attributes :name, :city, required: true
208 |
209 | association :speakers do
210 | attribute :name, :occupation, required: true
211 |
212 | association :presentation do
213 | attribute :topic, :duration, required: true
214 | end
215 | end
216 | end
217 | ```
218 |
219 | ### has_one: Rendering
220 |
221 | Use `fields_for` in a Rails environment to correctly setup the structure of params.
222 |
223 | ```erb
224 | <%= form_for @conference_form |f| %>
225 | <%= f.text_field :name %>
226 | <%= f.text_field :city %>
227 |
228 | <%= f.fields_for :speakers do |s| %>
229 | <%= s.text_field :name %>
230 | <%= s.text_field :occupation %>
231 |
232 | <%= s.fields_for :presentation do |p| %>
233 | <%= p.text_field :topic %>
234 | <%= p.text_field :duration %>
235 | <% end %>
236 | <% end %>
237 | <% end %>
238 | ```
239 |
240 | ## Dynamically Adding/Removing Nested Objects
241 |
242 | Action Form comes with two helpers to deal with this functionality:
243 |
244 | 1. `link_to_add_association` will display a link that renders fields to create a new object.
245 | 2. `link_to_remove_association` will display a link to remove a existing/dynamic object.
246 |
247 | In order to use it you have to insert this line: `//= require action_form` to your `app/assets/javascript/application.js` file.
248 |
249 | In our `ConferenceForm` we can dynamically create/remove `Speaker` objects. To do that we would write in the `app/views/conferences/_form.html.erb` partial:
250 |
251 | ```erb
252 | <%= form_for @conference_form do |f| %>
253 | <% if @conference_form.errors.any? %>
254 |
255 |
<%= pluralize(@conference_form.errors.count, "error") %> prohibited this conference from being saved:
256 |
257 |
258 | <% @conference_form.errors.full_messages.each do |message| %>
259 |
325 | ```
326 |
327 | ## Plain Old Ruby Object Forms
328 |
329 | ActionForm also can accept `ActiveModel::Model` instances as a model.
330 |
331 | ```ruby
332 | class Feedback
333 | include ActiveModel::Model
334 |
335 | attr_accessor :name, :body, :email
336 |
337 | def save
338 | FeedbackMailer.send_email(email, name, body)
339 | end
340 | end
341 | ```
342 |
343 | The form should look like this.
344 |
345 | ```ruby
346 | class FeedbackForm < ActionForm::Base
347 | attributes :name, :body, :email, required: true
348 | end
349 | ```
350 |
351 | And then in controller:
352 |
353 | ```ruby
354 | class FeedbacksController
355 | def create
356 | feedback = Feedback.new
357 | @feedback_form = FeedbackForm.new(feedback)
358 | @feedback_form.submit(feedback_params)
359 |
360 | if @feedback_form.save
361 | head :ok
362 | else
363 | render json: @feedback_form.errors
364 | end
365 | end
366 | ```
367 |
368 | ## Demos
369 |
370 | You can find a list of applications using this gem in this repository: https://github.com/m-Peter/nested-form-examples .
371 | All the examples are implemented in before/after pairs. The before is using the `accepts_nested_attributes_for`, while the after uses this gem to achieve the same functionality.
372 |
373 | ## Credits
374 |
375 | Special thanks to the owners of the great gems that inspired this work:
376 |
377 | * [Nick Sutterer](https://github.com/apotonick) - creator of [reform](https://github.com/apotonick/reform)
378 | * [Nathan Van der Auwera](https://github.com/nathanvda) - creator of [cocoon](https://github.com/nathanvda/cocoon)
379 |
--------------------------------------------------------------------------------