Marking multi-level nested forms as "dirty" in Rails - ruby-on-rails

I have a three-level multi-nested form in Rails. The setup is like this: Projects have many Milestones, and Milestones have many Notes. The goal is to have everything editable within the page with JavaScript, where we can add multiple new Milestones to a Project within the page, and add new Notes to new and existing Milestones.
Everything works as expected, except that when I add new notes to an existing Milestone (new Milestones work fine when adding notes to them), the new notes won't save unless I edit any of the fields that actually belong to the Milestone to mark the form "dirty"/edited.
Is there a way to flag the Milestone so that the new Notes that have been added will save?
Edit: sorry, it's hard to paste in all of the code because there's so many parts, but here goes:
Models
class Project < ActiveRecord::Base
has_many :notes, :dependent => :destroy
has_many :milestones, :dependent => :destroy
accepts_nested_attributes_for :milestones, :allow_destroy => true
accepts_nested_attributes_for :notes, :allow_destroy => true, :reject_if => proc { |attributes| attributes['content'].blank? }
end
class Milestone < ActiveRecord::Base
belongs_to :project
has_many :notes, :dependent => :destroy
accepts_nested_attributes_for :notes, :allow_destroy => true, :allow_destroy => true, :reject_if => proc { |attributes| attributes['content'].blank? }
end
class Note < ActiveRecord::Base
belongs_to :milestone
belongs_to :project
scope :newest, lambda { |*args| order('created_at DESC').limit(*args.first || 3) }
end
I'm using an jQuery-based, unobtrusive version of Ryan Bates' combo helper/JS code to get this done.
Application Helper
def add_fields_for_association(f, association, partial)
new_object = f.object.class.reflect_on_association(association).klass.new
fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
render(partial, :f => builder)
end
end
I render the form for the association in a hidden div, and then use the following JavaScript to find it and add it as needed.
JavaScript
function addFields(link, association, content, func) {
var newID = new Date().getTime();
var regexp = new RegExp("new_" + association, "g");
var form = content.replace(regexp, newID);
var link = $(link).parent().next().before(form).prev();
if (func) {
func.call();
}
return link;
}
I'm guessing the only other relevant piece of code that I can think of would be the create method in the NotesController:
def create
respond_with(#note = #owner.notes.create(params[:note])) do |format|
format.js { render :json => #owner.notes.newest(3).all.to_json }
format.html { redirect_to((#milestone ? [#project, #milestone, #note] : [#project, #note]), :notice => 'Note was successfully created.') }
end
end
The #owner ivar is created in the following before filter:
def load_milestone
#milestone = #project.milestones.find(params[:milestone_id]) if params[:milestone_id]
end
def determine_owner
#owner = load_milestone || #project
end
Thing is, all this seems to work fine, except when I'm adding new notes to existing milestones. The milestone has to be "touched" in order for new notes to save, or else Rails won't pay attention.

This is bug #4242 in Rails 2.3.5 and it has been fixed in Rails 2.3.8.

i think your models are wrong. the notes have no direct relationship to project. they are through milestones.
try these
class Project < ActiveRecord::Base
has_many :milestones, :dependent => :destroy
has_many :notes, :through => :milestones
accepts_nested_attr ibutes_for :milestones, :allow_destroy => true
end
class Milestone < ActiveRecord::Base
belongs_to :project
has_many :notes, :dependent => :destroy
accepts_nested_attributes_for :notes, :allow_destroy => true, :reject_if => proc { |attributes| attributes['content'].blank? }
end
class Note < ActiveRecord::Base
belongs_to :milestone
end
Update: here is the code that worked for me based on the new info:
## project controller
# PUT /projects/1
def update
#project = Project.find(params[:id])
if #project.update_attributes(params[:project])
redirect_to(#project)
else
render :action => "edit"
end
end
# GET /projects/1/edit
def edit
#project = Project.find(params[:id])
#project.milestones.build
for m in #project.milestones
m.notes.build
end
#project.notes.build
end
## edit.html.erb
<% form_for(#project) do |f| %>
<%= f.error_messages %>
<p>
<%= f.label :name %><br />
<%= f.text_field :name %>
</p>
<% f.fields_for :notes do |n| %>
<p>
<div>
<%= n.label :content, 'Project Notes:' %>
<%= n.text_area :content, :rows => 3 %>
</div>
</p>
<% end %>
<% f.fields_for :milestones do |m| %>
<p>
<div>
<%= m.label :name, 'Milestone:' %>
<%= m.text_field :name %>
</div>
</p>
<% m.fields_for :notes do |n| %>
<p>
<div>
<%= n.label :content, 'Milestone Notes:' %>
<%= n.text_area :content, :rows => 3 %>
</div>
</p>
<% end %>
<% end %>
<p>
<%= f.submit 'Update' %>
</p>
<% end %>

Related

Multiple belongs_to to same model only updating for last id in form

So I have a User model and a Contract model with a many to many relationship. Contract belongs to multiple users but with different ids so i can do this
Contract.find(params[:id]).creator.email
Contract.find(params[:id]).leader.email
Contract.find(params[:id]).buyer.email
Contract.find(params[:id]).seller.email
User.find(params[:id]).contracts
In my form I have this, so I can set each id to an already created user
<%= simple_form_for(#contract) do |f| %>
<% if #contract.errors.any? %>
<div id="error_explanation">
<h3><%= pluralize(#contract.errors.count, 'error') %> prohibited this contract from being saved:</h3>
<ul>
<% #contract.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group">
<%= f.label :creator_id %><br>
<%= f.select(:creator_id, User.all.map { |c| [c.email, c.id] }) %>
</div>
<div class="form-group">
<%= f.label :leader_id %><br>
<%= f.select(:leader_id, User.all.map { |c| [c.email, c.id] }) %>
</div>
<div class="form-group">
<%= f.label :buyer_id %><br>
<%= f.select(:buyer_id, User.all.map { |c| [c.email, c.id] }) %>
</div>
<div class="form-group">
<%= f.label :seller_id %><br>
<%= f.select(:seller_id, User.all.map { |c| [c.email, c.id] }) %>
</div>
<% end %>
My contract is created with each user id correctly set but when I check the user's contracts, I can only find the same contract if user was the seller (so the last div in the form)
Edit:
Here's my two models.
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :contract, :foreign_key => 'creator_id'
has_many :contract, :foreign_key => 'leader_id'
has_many :contract, :foreign_key => 'buyer_id'
has_many :contract, :foreign_key => 'seller_id'
validates :email, presence: true, uniqueness: true
validates :role, presence: true
end
class Contract < ApplicationRecord
belongs_to :creator, :class_name => 'User', :foreign_key => 'creator_id'
belongs_to :leader, :class_name => 'User', :foreign_key => 'leader_id'
belongs_to :buyer, :class_name => 'User', :foreign_key => 'buyer_id'
belongs_to :seller, :class_name => 'User', :foreign_key => 'seller_id'
end
Contract controller:
class ContractsController < ApplicationController
before_action :set_contract, only: [:show, :edit, :update, :destroy]
def index
#contracts = Contract.all
end
def show
#contract = Contract.find(params[:id])
end
def new
#contract = Contract.new
end
def edit
end
def create
#contract = Contract.new(contract_params)
if #contract.save
redirect_to contracts_path, notice: 'Le contract a été créé avec succès'
else
render :new
end
end
def update
if #contract.update(contract_params)
redirect_to contract_path
else
render :edit
end
end
def destroy
#contract.destroy
redirect_to contracts_path
end
private
def set_contract
#contract = Contract.find(params[:id])
end
def contract_params
params.require(:contract).permit(:creator_id,
:leader_id,
:buyer_id,
:seller_id)
end
end
I need to have each of the selectors updating the chosen user but only the last one is working and I can't figure out how to make it work.
I feel like this approach can work but I'm new in rails and maybe I'm using the wrong method.
I tried a simple has_and_belongs_to_many relationship, creating users like explained here http://guides.rubyonrails.org/form_helpers.html under 'Building Complex Forms' but I lost the distinction between each specific user in the form when I set them with a field-for
Hope I'm clear enough, thanks !
Alright, so you have a couple of problems with your model definition.
Your relationship needs to be plural if you are using has_many, so it'll be has_many :contracts. This is most probably what is causing your problem.
It's not the best idea to have multiple foreign keys to the same table. Instead have one has_many :contracts, foreign_key: 'contractor_id'. But then use a separate variable to define the role of the User. And in Contract you'll have belongs_to :user, :foreign_key => 'contractor_id'. You can then specify custom accessor_methods if you want to call creator, leader, etc

Nested Model Form. Belongs_to class not displaying

I have a Subject Model and a Lesson Model.
I implemented a nested model form.
After subject creation, I led it to a page where it supposedly shows the lessons associated. However, I fail to see the lessons.
I believe the data for lessons did not get saved properly as
When I did a for example lesson.find_by_subject_id('1'), I get 'nil' in return.
I am trying to figure out how polymorphism works on rails and would appreciate it if someone could either point out where I've gone wrong or give me some guidance on how those to pass the values for belong_to classes to be created.
Subject Model
attr_accessible :subjectCode, :lessons_attributes
has_many :lessons, :dependent => :destroy
accepts_nested_attributes_for :lessons, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true
Lesson Model
attr_accessible :lessonName, :subject, :subject_id
belongs_to :subject
Subject Controller
def new
3.times {#subject.lessons.build}
end
def create
#subject = Subject.new(params[:subject])
if #subject.save
redirect_to #subject, :notice => "Successfully created subject."
else
render :action => 'new'
end
end
Form
<%= form_for #subject do |f| %>
<%= f.error_messages %>
<p>
<%= f.label :subjectCode %><br />
<%= f.text_field :subjectCode %>
</p>
<%= f.fields_for :lessons do |builder| %>
<p>
<%= builder.label :lessonName %> <br/>
<%= builder.text_area :lessonName, :rows=>3 %>
</p>
<% end %>
<p><%= f.submit "Submit" %></p>
<% end %>
Routes
resources :subjects do resources :lessons end
Your reject_if lambda will always reject the lessons attributes because lessons don't have a content attribute, so you're essentially evaluating nil.blank? which will return true
Perhaps you want to check if the lesson name is blank? Ala :reject_if => lambda { |a| a[:lessonName].blank? }
You don't have a field for content of lesson on form, so content will be blank with every lesson. And you also use:
:reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true
This will check if content is blank, the subject will not save the lesson. This is your problem, because you don't have a content field on your form, so content will blank every time you create subject, you used :reject_if so subject will not save its lesson. If you wan user can put content of lesson later, remove :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true and your lesson will be save with associated subject.

Rails Nested Attribute Retrieval in Controller

Here's the structure of my code. I have a video attached with each cresponse and as far as I can tell I have been successful in uploading it. The problem comes when I need to convert it after the structure is saved. I wish to access the newly updated nested attribute (see lesson_controller) but am not sure how to go about doing so.
Many thanks!
Pier.
lesson.rb
class Lesson < ActiveRecord::Base
has_one :user
has_many :comments, :dependent => :destroy
has_many :cresponses, :dependent => :destroy
acts_as_commentable
accepts_nested_attributes_for :comments, :reject_if => lambda { |a| a[:body].blank? }, :allow_destroy => true
accepts_nested_attributes_for :cresponses
and here's cresponse.rb
class Cresponse < ActiveRecord::Base
belongs_to :lesson
attr_accessible :media, :accepted, :description, :user_id
# NOTE: Comments belong to a user
belongs_to :user, :polymorphic => true
# Paperclip
require 'paperclip'
has_attached_file :media,
:url => "/system/:lesson_id/:class/:basename.:extension",
:path => ":rails_root/public/system/:lesson_id/:class/:basename.:extension"
Here's my HTML view
<% #cresponse = #lesson.cresponses.build %>
<%= form_for #lesson, :html => { :multipart => true } do |f| %>
<td class="list_discussion" colspan="2">
<div class="field">
<%= f.fields_for :cresponses, #cresponse, :url => #cresponse, :html => { :multipart => true } do |builder| %>
Upload : <%= builder.file_field :media %><br />
Description : <%= builder.text_field :description %>
<%= builder.hidden_field :user_id , :value => current_user.id %>
<% end %>
</div>
</td>
and here's lesson_controller.rb - update
def update
#lesson = Lesson.find(params[:id])
respond_to do |format|
if #lesson.update_attributes(params[:lesson])
**if #lesson.cresponses.** <-- not sure how to find the cresponse that I need to convert
puts("Gotta convert this")
end
Think I should answer my own question ..
Basically for lesson_controller.rb
params[:lesson][:cresponses_attributes].values.each do |cr|
#cresponse_user_id = cr[:user_id]
#cresponse_description = cr[:description]
if cr[:media]
.... and so on

Rails has_many through form with checkboxes and extra field in the join model

I'm trying to solve a pretty common (as I thought) task.
There're three models:
class Product < ActiveRecord::Base
validates :name, presence: true
has_many :categorizations
has_many :categories, :through => :categorizations
accepts_nested_attributes_for :categorizations
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
validates :description, presence: true # note the additional field here
end
class Category < ActiveRecord::Base
validates :name, presence: true
end
My problems begin when it comes to Product new/edit form.
When creating a product I need to check categories (via checkboxes) which it belongs to. I know it can be done by creating checkboxes with name like 'product[category_ids][]'. But I also need to enter a description for each of checked relations which will be stored in the join model (Categorization).
I saw those beautiful Railscasts on complex forms, habtm checkboxes, etc. I've been searching StackOverflow hardly. But I haven't succeeded.
I found one post which describes almost exactly the same problem as mine. And the last answer makes some sense to me (looks like it is the right way to go). But it's not actually working well (i.e. if validation fails). I want categories to be displayed always in the same order (in new/edit forms; before/after validation) and checkboxes to stay where they were if validation fails, etc.
Any thougts appreciated.
I'm new to Rails (switching from CakePHP) so please be patient and write as detailed as possible. Please point me in the right way!
Thank you. : )
Looks like I figured it out! Here's what I got:
My models:
class Product < ActiveRecord::Base
has_many :categorizations, dependent: :destroy
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations, allow_destroy: true
validates :name, presence: true
def initialized_categorizations # this is the key method
[].tap do |o|
Category.all.each do |category|
if c = categorizations.find { |c| c.category_id == category.id }
o << c.tap { |c| c.enable ||= true }
else
o << Categorization.new(category: category)
end
end
end
end
end
class Category < ActiveRecord::Base
has_many :categorizations, dependent: :destroy
has_many :products, through: :categorizations
validates :name, presence: true
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
validates :description, presence: true
attr_accessor :enable # nice little thingy here
end
The form:
<%= form_for(#product) do |f| %>
...
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :categorizations, #product.initialized_categorizations do |builder| %>
<% category = builder.object.category %>
<%= builder.hidden_field :category_id %>
<div class="field">
<%= builder.label :enable, category.name %>
<%= builder.check_box :enable %>
</div>
<div class="field">
<%= builder.label :description %><br />
<%= builder.text_field :description %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
And the controller:
class ProductsController < ApplicationController
# use `before_action` instead of `before_filter` if you are using rails 5+ and above, because `before_filter` has been deprecated/removed in those versions of rails.
before_filter :process_categorizations_attrs, only: [:create, :update]
def process_categorizations_attrs
params[:product][:categorizations_attributes].values.each do |cat_attr|
cat_attr[:_destroy] = true if cat_attr[:enable] != '1'
end
end
...
# all the rest is a standard scaffolded code
end
From the first glance it works just fine. I hope it won't break somehow.. :)
Thanks all. Special thanks to Sandip Ransing for participating in the discussion. I hope it will be useful for somebody like me.
use accepts_nested_attributes_for to insert into intermediate table i.e. categorizations
view form will look like -
# make sure to build product categorizations at controller level if not already
class ProductsController < ApplicationController
before_filter :build_product, :only => [:new]
before_filter :load_product, :only => [:edit]
before_filter :build_or_load_categorization, :only => [:new, :edit]
def create
#product.attributes = params[:product]
if #product.save
flash[:success] = I18n.t('product.create.success')
redirect_to :action => :index
else
render_with_categorization(:new)
end
end
def update
#product.attributes = params[:product]
if #product.save
flash[:success] = I18n.t('product.update.success')
redirect_to :action => :index
else
render_with_categorization(:edit)
end
end
private
def build_product
#product = Product.new
end
def load_product
#product = Product.find_by_id(params[:id])
#product || invalid_url
end
def build_or_load_categorization
Category.where('id not in (?)', #product.categories).each do |c|
#product.categorizations.new(:category => c)
end
end
def render_with_categorization(template)
build_or_load_categorization
render :action => template
end
end
Inside view
= form_for #product do |f|
= f.fields_for :categorizations do |c|
%label= c.object.category.name
= c.check_box :category_id, {}, c.object.category_id, nil
%label Description
= c.text_field :description
I just did the following. It worked for me..
<%= f.label :category, "Category" %>
<%= f.select :category_ids, Category.order('name ASC').all.collect {|c| [c.name, c.id]}, {} %>

Rails 3, nested multi-level forms and has_many through

I'm trying to get it to work but it dosen't!
I have
class User < ActiveRecord::Base
has_many :events, :through => :event_users
has_many :event_users
accepts_nested_attributes_for :event_users
end
class Event < ActiveRecord::Base
has_many :event_users
has_many :users, :through => :event_users
accepts_nested_attributes_for :users
end
class EventUser < ActiveRecord::Base
set_table_name :events_users
belongs_to :event
belongs_to :user
accepts_nested_attributes_for :events
accepts_nested_attributes_for :users
end
And also the table-layout
event_users
user_id
event_id
user_type
events
id
name
users
id
name
And this is my form
<%= semantic_form_for #event do |f| %>
<%= f.semantic_fields_for :users, f.object.users do |f1| %>
<%= f1.text_field :name, "Name" %>
<%= f1.semantic_fields_for :event_users do |f2| %>
<%= f2.hidden_field :user_type, :value => 'participating' %>
<% end %>
<% end %>
<%= link_to_add_association 'add task', f, :users %>
<% end %>
The problem is that if I create a new user this way, it doesn't set the value of user_type (but it creates a user and a event_users with user_id and event_id). If I go back to the edit-form after the creation of a user and submit, then the value of user_type is set in events_users. (I have also tried without formtastic)
Any suggestions? Thanks!
----edit----
I have also tried to have the event_users before users
<%= semantic_form_for #event do |f| %>
<%= f.semantic_fields_for :event_users do |f1| %>
<%= f1.hidden_field :user_type, :value => 'participating' %>
<%= f1.semantic_fields_for :users do |f2| %>
<%= f2.text_field :name, "Name" %>
<% end %>
<% end %>
<%= link_to_add_association 'add task', f, :event_users %>
<% end %>
but then it only throws me an error:
User(#2366531740) expected, got
ActiveSupport::HashWithIndifferentAccess(#2164210940)
--edit--
the link_to_association is a formtastic-cocoon method (https://github.com/nathanvda/formtastic-cocoon) but I have tried to do other approaches but with the same result
---edit----
def create
#event = Event.new(params[:event])
respond_to do |format|
if #event.save
format.html { redirect_to(#event, :notice => 'Event was successfully created.') }
format.xml { render :xml => #event, :status => :created, :location => #event }
else
format.html { render :action => "new" }
format.xml { render :xml => #event.errors, :status => :unprocessable_entity }
end
end
end
To be honest, i have never tried to edit or create a has_many :through in that way.
It took a little while, and had to fix the js inside formtastic_cocoon to get it working, so here is a working solution.
You need to specift the EventUser model, and then fill the User model (the other way round will never work).
So inside the models you write:
class Event < ActiveRecord::Base
has_many :event_users
has_many :users, :through => :event_users
accepts_nested_attributes_for :users, :reject_if => proc {|attributes| attributes[:name].blank? }, :allow_destroy => true
accepts_nested_attributes_for :event_users, :reject_if => proc {|attributes| attributes[:user_attributes][:name].blank? }, :allow_destroy => true
end
class EventUser < ActiveRecord::Base
belongs_to :user
belongs_to :event
accepts_nested_attributes_for :user
end
class User < ActiveRecord::Base
has_many :events, :through => :event_users
has_many :event_users
end
Then the views. Start with the events/_form.html.haml
= semantic_form_for #event do |f|
- f.inputs do
= f.input :name
%h3 Users (with user-type)
#users_with_usertype
= f.semantic_fields_for :event_users do |event_user|
= render 'event_user_fields', :f => event_user
.links
= link_to_add_association 'add user with usertype', f, :event_users
.actions
= f.submit 'Save'
(i ignore errors for now)
Then, you will need to specify the partial _event_user_fields.html.haml partial (here comes a little bit of magic) :
.nested-fields
= f.inputs do
= f.input :user_type, :as => :hidden, :value => 'participating'
- if f.object.new_record?
- f.object.build_user
= f.fields_for(:user, f.object.user, :child_index => "new_user") do |builder|
= render("user_fields", :f => builder, :dynamic => true)
and to end the _user_fields partial (which does not really have to be a partial)
.nested-fields
= f.inputs do
= f.input :name
This should work.
Do note that i had to update the formtastic_cocoon gem, so you will need to update to version 0.0.2.
Now it would be easily possible to select the user_type from a simple dropdown, instead of a hidden field, e.g. use
= f.input :user_type, :as => :select, :collection => ["Participator", "Organizer", "Sponsor"]
Some thoughts (now i proved it works):
this will always create new users on the fly, actually eliminating the need for the EventUser. Will you allow selecting existing users from a dropdown too?
personally i would turn it around: let users assign themselves to an event!
Does the events_users model not have an ID column? Since there's an additional field (user_type) then EventUser is a model and should probably have an ID. Maybe that's why user_type isn't being set in your first case.

Resources