I have a site that keeps track of SAT tutoring sessions. The curriculum that the students learn is a collection of rules. I have a model for each tutoring session called "Sittings" and the rules model is called "Rules". I want the site admin to be able to enter a Sitting by date, and then use checkboxes to select which "rules" the student got wrong in that sitting. I'm a little confused as to how I can create the form to pull out specific rules without adding attributes to my Sitting model of rule1, rule2, etc. I'm using simple_form to create my forms.
My Sitting model:
class Sitting < ActiveRecord::Base
attr_accessible :date, :comment, :rule_id, :user_id
validates :date, presence: true
belongs_to :user
has_many :combos
has_many :rules, :through => :combos
end
My Rules model:
class Rule < ActiveRecord::Base
attr_accessible :name, :subject, :session_id, :hint_id, :question_id, :trigger_id
validates :name, presence: true
validates :subject, presence: true
has_many :questions
has_many :triggers
has_many :hints
has_many :combos
has_many :sittings, :through => :combos
end
My Combo model:
class Combo < ActiveRecord::Base
belongs_to :sitting
belongs_to :rule
end
Edit:
Here's what I have tried for the form. It does create the checkbox form, but my DB isn't updating the rule_id. (shows as nil when I create a Sitting)
form:
<%= simple_form_for(#sitting, html: { class: "form-horizontal"}) do |f| %>
<%= f.error_notification %>
<% Rule.all.each do |rule| %>
<%= check_box_tag "sitting[rule_ids][]", rule.id, #sitting.rule_ids.include?(rule.id) %> <%= rule.id %>
<% end %>
<div class="form-group">
<%= f.input :comment, as: :text, input_html: { rows: "2", :class => "form-control" }, label: "Comments:" %>
</div>
<div class="form-group">
<%= f.date_select :date, as: :date, label: "Taken Date:" %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
I updated my strong params to allow an array:
def create
#sitting = Sitting.new(sitting_params)
respond_to do |format|
if #sitting.save
format.html { redirect_to #sitting, notice: 'Sitting was successfully created.' }
format.json { render action: 'show', status: :created, location: #sitting }
else
format.html { render action: 'new' }
format.json { render json: #sitting.errors, status: :unprocessable_entity }
end
end
end
def sitting_params
params.require(:sitting).permit(:comment, :date, :user_id, :rule_id => [])
end
Am I missing something in order to update the Sitting.rule_id properly? I get the following error in my logs:
WARNING: Can't mass-assign protected attributes for Sitting: rule_ids
app/controllers/sittings_controller.rb:27:in `create'
Just to summarize what we got through over the chat.
Firstly, you do not need both attr_accessible and strong_params at the same time. I've posted another answer some time ago explaining how those two approaches differs from each other.
You're running rails 4, so you should take advantage of strong params and not use protected_attributes gem. In short, remove this gem from you Gemfile as well as all attr_accessible calls.
As Marian noticed, you have a typo in your strong params method, you need to permit rule_ids, not rule_id. rule_id column is obsolete, as sitting has_many :rules :through rather than sitting belongs_to :rule - most likely it is an artifact of old association code.
As soon as rule_ids are assigned within your model, it will create new join models in your join table, creating an association between given sitting and passed rules.
Related
I have a dilemma...I have put together a multi-tenant app and I'm having an issue with one form and action. The form (Profile) has a collection select where one selects the skills applicable. There is a many-to-many with Profile <- Taggings -> Tags. The Tag records appear in the collection for select, with multi-select enabled.
When you select some skills and save the profile record, in the profile update action, Rails throws:
Validation failed: Tenant must exist
This seems to be coming from the Taggings table, as when I remove the relationship with Tenant, no error and the profile record saves successfully. If I create a tagging record directly from Rails console, the tenant_id populates and the record saves.
respond_to do |format|
if #profile.update(profile_params) < fails here
format.html { redirect_to profiles_path, notice: 'Profile was successfully updated.' }
format.json { render :show, status: :ok, location: #profile }
else
Params (if I must insert tenant_id here, I expect within the tag id array, but how?):
{"utf8"=>"✓",
"_method"=>"patch",
"authenticity_token"=>"longobscurestring",
"profile"=>
{"description"=>"",
"tag_ids"=>
["",
"b39b38eb-a90b-434b-9457-1f3b67cee54e",
"08d90ee7-3194-4fec-acee-bcecfae1e8e7",
"ee8de96d-1206-4d73-bcf0-0b99f995569a",
"469ce954-b2bd-49d5-9dbc-0636b4da43c8",
"38b90691-d3f0-4c9d-8b5f-2c644a894d45",
"77a332d9-feed-4f88-8133-19066b5d33bc",
"05c145ce-a8ff-4105-a713-073da60184b5",
"8d6f98e3-9c3e-4f45-8c7d-36b177b557af"],
"contact_direct"=>"false"},
"commit"=>"Save",
"id"=>"1728fcc4-f2e2-49de-9a39-5c67502b8a85"}
Profile form:
<%= simple_form_for #profile, url: profile_path do |f| %>
<%= f.input :description, :as => :text, :input_html => {:cols=>100, :rows=>5} %>
<%= f.input :tag_ids, label: 'Skills', as: :select, collection: Tag.active.order(:name), label_method: :name, input_html: { multiple: true } %>
<div>
<span>People may contact me:</br></span>
<div class="radio-inline margin-10" style="text-align:left">
<%= f.input :contact_direct, label: '', as: :radio_buttons, collection: [['Directly', true], ['Through my manager', false]] %>
</div>
</div>
<br/>
<div class="actions">
<% if ( can? :manage, :all or current_user.id == #profile.user_id) %>
<%= f.button :submit, 'Save', class: "btn btn-success" %>
<% end %>
<%= link_to "Back", profiles_path(:search => {:column => 'First', :direction => 'Up', :name => ''}), class: "btn btn-primary link-as-button" %>
</div>
<% end %>
Profile model associations:
class Profile < ApplicationRecord
belongs_to :user
belongs_to :tenant
has_many :taggings, :dependent => :destroy
has_many :tags, through: :taggings
Taggings model associations:
class Tagging < ApplicationRecord
attribute :competence, :integer, default: 0
belongs_to :tenant
belongs_to :tag
belongs_to :profile, optional: true
has_one :user, through: :profile
has_many :endorsements, inverse_of: 'tagging'
has_many :endorsers, through: :endorsements
All tables have had RLS implemented through pg policies. Without tenant_id on the taggings record though, one can access the record through another tenant.
Please let me know anything else required here to debug. Thankyou in advance!
I managed to solve this by adding into the Tagging model an after_initialize callback to the following:
def set_tenant_id
if new_record?
self.tenant_id = self.tag.tenant_id
end
end
Feel free to say if this is bad practice...but I can't see any other way at present.
I am using simple_form along with the nested_form gems. The simple_form validations are show for the parent form but are not showing for an association within the nested form.
View
#_form.html.erb
<%= simple_nested_form_for( #repair ) do |r| %>
....
<%= r.simple_fields_for :repair_items do |f| %>
<%= f.association :repair_type, label_method: :repair_type_label, value_method: :id, include_blank: true, label: "Type"%>
Model
#repair_item.rb
validates :repair_type_id, :presence => true
Controller
#repairs_controller.rb
def create
#repair = Repair.new(params[:repair])
if #repair.save
redirect_to(repairs_path, :notice => 'Repair Created.')
else
#repair.repair_items.new(params[:repair][:repair_items_attributes]["0"].except(:_destroy))
render :new
end
end
Edit
I failed to mention that the association within the nested form is displayed using the select2 js plugin. This may be a factor as to why the Rails validations are not showing???
In you model add inverse_of
class Repair < ActiveRecord::base
has_many :repair_types, inverse_of: :repair
end
class RepairType < ActiveRecord::base
belongs_to :repair, inverse_of: repair_types
end
I'm attempting to build a recipe-keeper app with three primary models:
Recipe - The recipe for a particular dish
Ingredient - A list of ingredients, validated on uniqueness
Quantity - A join table between Ingredient and Recipe that also reflects the amount of a particular ingredient required for a particular recipe.
I'm using a nested form (see below) that I constructed using an awesome Railscast on Nested Forms (Part 1, Part 2) for inspiration. (My form is in some ways more complex than the tutorial due to the needs of this particular schema, but I was able to make it work in a similar fashion.)
However, when my form is submitted, any and all ingredients listed are created anew—and if the ingredient already exists in the DB, it fails the uniqueness validation and prevents the recipe from being created. Total drag.
So my question is: Is there a way to submit this form so that if an ingredient exists whose name matches one of my ingredient-name fields, it references the existing ingredient instead of attempting to create a new one with the same name?
Code specifics below...
In Recipe.rb:
class Recipe < ActiveRecord::Base
attr_accessible :name, :description, :directions, :quantities_attributes,
:ingredient_attributes
has_many :quantities, dependent: :destroy
has_many :ingredients, through: :quantities
accepts_nested_attributes_for :quantities, allow_destroy: true
In Quantity.rb:
class Quantity < ActiveRecord::Base
attr_accessible :recipe_id, :ingredient_id, :amount, :ingredient_attributes
belongs_to :recipe
belongs_to :ingredient
accepts_nested_attributes_for :ingredient
And in Ingredient.rb:
class Ingredient < ActiveRecord::Base
attr_accessible :name
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
Here's my nested form that displays at Recipe#new:
<%= form_for #recipe do |f| %>
<%= render 'recipe_form_errors' %>
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<h3>Ingredients</h3>
<div id='ingredients'>
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient_attributes do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name %>
<% end %>
<%= ff.label :amount %>
<%= ff.text_field :amount, size: "10" %>
<%= ff.hidden_field :_destroy %>
<%= link_to_function "remove", "remove_fields(this)" %><br>
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
</div><br>
<%= f.label :description %><br>
<%= f.text_area :description, rows: 4, columns: 100 %><br>
<%= f.label :directions %><br>
<%= f.text_area :directions, rows: 4, columns: 100 %><br>
<%= f.submit %>
<% end %>
The link_to and link_to_function are there to allow the addition and removal of quantity/ingredient pairs on the fly, and were adapted from the Railscast mentioned earlier. They could use some refactoring, but work more or less as they should.
Update: Per Leger's request, here's the relevant code from recipes_controller.rb. In the Recipes#new route, 3.times { #recipe.quantities.build } sets up three blank quantity/ingredient pairs for any given recipe; these can be removed or added to on the fly using the "Add ingredient" and "remove" links mentioned above.
class RecipesController < ApplicationController
def new
#recipe = Recipe.new
3.times { #recipe.quantities.build }
#quantity = Quantity.new
end
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
redirect_to #recipe
else
render :action => 'new'
end
end
You shouldn't put the logic of ingredients match into view - it's duty of Recipe#create to create proper objects before passing 'em to Model. Pls share the relevant code for controller
Few notes before coming to code:
I use Rails4#ruby2.0, but tried to write Rails3-compatible code.
attr_acessible was deprecated in Rails 4, so strong parameters are used instead. If you ever think to upgrade your app, just go with strong parameters from the beginning.
Recommend to make Ingredient low-cased to provide uniform appearance on top of case-insensitivity
OK, here we go:
Remove attr_accessible string in Recipe.rb, Quantity.rb and Ingredient.rb.
Case-insensitive, low-cased Ingredient.rb:
class Ingredient < ActiveRecord::Base
before_save { self.name.downcase! } # to simplify search and unified view
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
end
<div id='ingredients'> part of adjusted form to create/update Recipe:
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name, size: "10" %>
<% end %>
...
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
We should use :ingredient from Quantity nested_attributes and Rails will add up _attributes-part while creating params-hash for further mass assignment. It allows to use same form in both new and update actions. For this part works properly association should be defined in advance. See adjusted Recipe#new bellow.
and finally recipes_controller.rb:
def new
#recipe = Recipe.new
3.times do
#recipe.quantities.build #initialize recipe -> quantities association
#recipe.quantities.last.build_ingredient #initialize quantities -> ingredient association
end
end
def create
#recipe = Recipe.new(recipe_params)
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
def update
#recipe = Recipe.find(params[:id])
#recipe.attributes = recipe_params
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
private
def prepare_recipe
#recipe.quantities.each do |quantity|
# do case-insensitive search via 'where' and building SQL-request
if ingredient = Ingredient.where('LOWER(name) = ?', quantity.ingredient.name.downcase).first
quantity.ingredient_id = quantity.ingredient.id = ingredient.id
end
end
end
def recipe_params
params.require(:recipe).permit(
:name,
:description,
:directions,
:quantities_attributes => [
:id,
:amount,
:_destroy,
:ingredient_attributes => [
#:id commented bc we pick 'id' for existing ingredients manually and for new we create it
:name
]])
end
In prepare_recipe we do the following things:
Find ID of ingredient with given name
Set foreign_key quantity.ingredient_id to ID
Set quantity.ingredient.id to ID (think what happens if you don't do that and change ingredient name in Recipe)
Enjoy!
I am trying to use the nested_form gem to create a dropdown that will show a list of chains and their locations. When the user clicks the submit button, the nested_form should create a new entry in a joiner table called users_locations. However, the form spits out an error as follows:
User locations user can't be blank
In other words, its complaining that the user_id is NULL and therefore it cannot create an entry in the table (because user_id is a required field in the user_locations table).
_form.html.erb (In the User view folder)
<div class="field" id="location_toggle">
<%= f.label "Location:", :class => "form_labels", required: true %>
<%= f.fields_for :user_locations do |location| %>
<%= location.grouped_collection_select :location_id, Chain.all, :locations, :name, :id, :name, include_blank: false %>
<% end %>
<%= f.link_to_add "Add a Location", :user_locations, :class => "btn btn-primary btn-small" %>
<br/>
</div>
user.rb (model)
belongs_to :chain
has_many :user_locations
has_many :locations, :through => :user_locations
...
accepts_nested_attributes_for :user_locations, :reject_if => :all_blank, :allow_destroy => true
validates_associated :user_locations, :message => ": you have duplicate entries."
...
attr_accessible :first_name, :last_name, ... , :user_locations_attributes
users_controller.rb
def create
#user = User.new(params[:user])
#user.save ? (redirect_to users_path, notice: 'User was successfully created.') : (render action: "new")
end
Log shows:
SELECT 1 AS one FROM "user_locations" WHERE ("user_locations"."user_id" IS NULL AND "user_locations"."location_id" = 1)
I'm assuming you're user_location validates :user_id, presence: true or something, right?
I had a look at this recently in response to this question, it seems that, when creating nested models, validations for all objects to be created are run before any of them are saved, and thus even though your model will get have user_id set when it is saved, it cannot have it when validated.
To get around this you may want to disable the validation on creation, for example with:
# user_locations.rb
validates :user_id, presence: true, on: :update
It looks like you're blending simple_form with the default Rails form helpers and getting some odd behavior. I haven't worked with simple_form, but my guess is that you can fix your problem by changing f.fields_for to f.simple_fields_for.
Here's an example that may help.
I've got a simple nested form working in my rails3 application. I'm trying to work out how to save a value from the parent model into the child when saving.
class Radcheck < ActiveRecord::Base
set_table_name 'radcheck'
attr_accessible :attribute_name, :username, :value, :op
belongs_to :raduser
end
class Raduser < ActiveRecord::Base
has_many :radcheck, :dependent => :destroy
accepts_nested_attributes_for :radcheck
end
And my form:
<%= form_for #raduser do |f| %>
<p>
<%= f.label :username %><br />
<%= f.text_field :username %>
</p>
<%= f.fields_for :radcheck do |builder| %>
<li>
<%= builder.label :attribute_name %><%= builder.text_field :attribute_name %>
</li>
<% end %>
<p><%= f.submit "Submit" %></p>
<% end %>
What I want to do is save the Raduser.username value in to the radcheck table on save. For each record.
I've tried putting something in the controller but that wasn't really working for us.
-- Update --
In my radcheck controller, I've tried this (as a test) but the values don't save.
def create
#radcheck = Radcheck.new(params[:radcheck])
#radcheck.username = '123'
respond_to do |format|
if #radcheck.save
format.html { redirect_to #radcheck, notice: 'Radcheck was successfully created.' }
format.json { render json: #radcheck, status: :created, radcheck: #radcheck }
else
format.html { render action: "new" }
format.json { render json: #radcheck.errors, status: :unprocessable_entity }
end
end
end
Have also tried in radusers but that gave error.
It looks like the controller code you posted is for the Radcheck controller, correct? If so, the form you have also posted will be using the create action of the RaduserController class, not the one from RadcheckController. This would explain why you aren't seeing the username '123' in the radcheck rows.
If the username field is the same between parent and child, a common way to sync these two up would be with an observer or a before_save callback. I'll outline the before_save method below:
class Radcheck < ActiveRecord::Base
set_table_name 'radcheck'
attr_accessible :attribute_name, :username, :value, :op
belongs_to :raduser
before_save :sync_usernames
private
def sync_usernames
self.username = self.raduser.username
end
end
I've tested this with Rails 3.1 and it works; however, I know I have run into issues accessing parent models within a child model in previous versions of Rails. In those cases, I had to put the before_save action on the parent model, and have it iterate over each child, setting the appropriate attributes that way.