Edit nested in a form - ruby-on-rails

Thanks to help on stackoverflow I got my create nested models form working the other day but I can't for the life of me get the corresponding update form to work. I have read a lot and tried out as many solutions as I can find.
The form looks fine but the nested attributes of Manufacturer and Scale, which are select via drop down, don't have their current values. All non nested elements of the form work fine.
Whatever changes you make to the two nested drop downs, pressing save changes creates NEW lines in the corresponding tables and doesn't alter the existing.
Ultimately what I want is for the attributes to be editable and then i'll have an "add manufacturer" and "add scale" button or link for miniatures that need multiple listings.
Here are my form fields where I've tried and failed to pass a hidden field.
Form
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :material %>
<%= f.select 'material', options_from_collection_for_select(Miniature.select("DISTINCT material"), :material, 'material', #miniature.material) %>
<%= f.fields_for :sizes do |size_fields| %>
<%= size_fields.label :scale_id, "Scale".pluralize %>
<%= hidden_field "Miniature Scale", #miniature.sizes %>
<%= size_fields.select :scale_id, options_from_collection_for_select(Scale.all, :id, :name) %>
<% end %>
<%= f.fields_for :productions do |production_fields| %>
<%= production_fields.label :manufacturer_id, "Manufacturer".pluralize %>
<%= hidden_field "Miniature Manufacturer", #miniature.productions %>
<%= production_fields.select :manufacturer_id, options_from_collection_for_select(Manufacturer.all, :id, :name, #miniature.manufacturers) %>
<% end %>
<%= f.label :release_date %>
<%= f.date_select :release_date, :start_year => Date.current.year, :end_year => 1970, :include_blank => true %>
Here is the miniatures controller where I'm pretty sure I've filled the 'def update' with too much/the wrong stuff.
Miniatures Controller
class MiniaturesController < ApplicationController
before_action :signed_in_user, only: [:new, :create, :edit, :update]
before_action :admin_user, only: :destroy
def show
#miniature = Miniature.find(params[:id])
end
def new
#miniature = Miniature.new
#miniature.productions.build
#miniature.sizes.build
end
def create
#miniature = Miniature.new(miniature_params)
#production = #miniature.productions.build
#size = #miniature.sizes.build
if #miniature.save
redirect_to #miniature
else
render 'new'
end
end
def edit
#miniature = Miniature.find(params[:id])
end
def update
#miniature = Miniature.find(params[:id])
#production = #miniature.productions.find(params[:id])
#size = #miniature.sizes.find(params[:id])
if #miniature.update_attributes(miniature_params)
#production = #miniature.productions.update_attributes(:manufacturer_id)
#size = #miniature.sizes.update_attributes(:scale_id)
flash[:success] = "Miniature updated"
redirect_to #miniature
else
render 'edit'
end
end
def index
#miniatures = Miniature.paginate(page: params[:page])
end
def destroy
Miniature.find(params[:id]).destroy
flash[:success] = "Miniature destroyed."
redirect_to miniatures_url
end
private
def miniature_params
params.require(:miniature).permit(:name, :release_date, :material, productions_attributes: [:manufacturer_id], sizes_attributes: [:scale_id])
end
def admin_user
redirect_to(root_url) unless current_user.admin?
end
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
end
I won't attach the models as I'm pretty sure the relationships are all correct since they work fine for creating new nested models.
Miniatures have_many scales and manufactures through sizes and productions.
Any help or pointers very much appreciated.

Thanks to an answer on this Q I've solved it. What I had already was fine for CREATING but didn't work for UPDATES because I hadn't whitelisted the JOIN model ids in the 'miniature_params, so they couldn't retrieve the existing info.
Now I have productions_attributes: [:id, :manufacturer_id] instead of just productions_attributes: [:manufacturer_id]
as below
def miniature_params
params.require(:miniature).permit(:name, :release_date, :material, productions_attributes: [:id, :manufacturer_id], sizes_attributes: [:id, :scale_id])
end
I can also strip ALL of the references to the nested models out of my miniatures controller update method as it 'just works'.
def update
#miniature = Miniature.find(params[:id])
if #miniature.update_attributes(miniature_params)
flash[:success] = "Miniature updated"
redirect_to #miniature
else
render 'edit'
end
end
Hope this is useful to someone in future.

Related

Saving a list of emails from a form-text input into Models email_list attribute (array)

My goal is to when adding a new product with the new product form, to have an input where one can add a list of emails separated by a space. The list of emails in this string field would be saved as an array of emails in the email_list array attribute of the Product model. This way each product has many emails. (later an email will be sent to these users to fill out questionaire, once a user fills it out there name will be taken off this list and put on completed_email_list array.
I am relatively new to rails, and have a few questions regarding implementing this. I am using postgresql, which from my understanding I do not need to serialize the model for array format because of this. Below is what I have tried so far to implement this. These may show fundamental flaws in my thinking of how everything works.
My first thinking was that I can in my controllers create action first take params[:email].split and save that directly into the email_list attribute (#product.email_list = params[:email].split. It turns out that params[:email] is always nil. Why is this? (this is a basic misunderstanding I have)(I put :email as accepted param).
After spending a long time trying to figure this out, I tried the following which it seems works, but I feel this is probably not the best way to do it (in the code below), which involves creating ANOTHER attribute of string called email, and then splitting it and saving it in the email_list array :
#product.email_list = #product.email.split
What is the best way to actually implement this? someone can clear my thinking on this I would be very grateful.
Cheers
Products.new View
<%= simple_form_for #product do |f| %>
<%= f.input :title, label:"Product title" %>
<%= f.input :description %>
<%= f.input :email %>
<%= f.button :submit %>
<%end %>
Products Controller
class ProductsController < ApplicationController
before_action :find_product, only: [:show, :edit, :update, :destroy]
def index
if params[:category].blank?
#products= Product.all.order("created_at DESC")
else
#category_id=Category.find_by(name: params[:category]).id
#products= Product.where(:category_id => #category_id).order("created_at DESC")
end
end
def new
#product=current_user.products.build
#categories= Category.all.map{|c| [c.name, c.id]}
end
def show
end
def edit
#categories= Category.all.map{|c| [c.name, c.id]}
end
def update
#product.category_id = params[:category_id]
if #product.update(product_params)
redirect_to product_path(#product)
else
render 'new'
end
end
def destroy
#product.destroy
redirect_to root_path
end
def create
#product=current_user.products.build(product_params)
#product.category_id = params[:category_id]
#product.email_list = #product.email.split
if #product.save
redirect_to root_path
else
render 'new'
end
end
private
def product_params
params.require(:product).permit(:title, :description, :category_id, :video, :thumbnail,:email, :email_list)
end
def find_product
#product = Product.find(params[:id])
end
end
To solve your original issue
#product.email_list = params[:email].split. It turns out that params[:email] is always nil
:email is a sub key of :product hash, so it should be:
#product.email_list = params[:product][:email].split
Demo:
params = ActionController::Parameters.new(product: { email: "first#email.com last#email.com" })
params[:email] # => nil
params[:product][:email] # => "first#email.com last#email.com"
I'd say that what you have is perfectly fine, except for the additional dance that you're doing in #product.email_list=#product.email.split, which seems weird.
Instead, I'd have an emails param in the form and an #emails= method in the model (rather than email and #email=):
def emails=(val)
self.email_list = val.split
end
Alternatively, you could do that in the controller rather than having the above convenience #emails= method, similar to the way you're handling the category_id:
#product = current_user.products.build(product_params)
#product.category_id = params[:category_id]
#product.email_list = product_params[:emails].split
Because you need validations on your emails and to make it cleaner I would create an email table, make Product table accept Email attribues and use cocoon gem to have a nice dynamic nested form with multiple emails inputs.
1) models
class Product < ActiveRecord::Base
has_many :emails, dependent: :destroy
accepts_nested_attributes_for :emails, reject_if: :all_blank, allow_destroy: true
end
class Email < ActiveRecord::Base
belong_to :product
validates :address, presence: true
end
2) Controller
class ProductsController < ApplicationController
def new
#product = current_user.products.build
end
def create
#product = current_user.products.build(product_params)
if #product.save
redirect_to root_path
else
render 'new'
end
end
private
def product_params
params.require(:project).permit(:title, :description, :category_id, :video, :thumbnail, emails_attributes: [:id, :address, :_destroy])
end
end
3) View
<%= simple_form_for #product do |f| %>
<%= f.input :title, label:"Product title" %>
<%= f.input :description %>
<%= f.association :category %>
<div id="emails">
<%= f.simple_fields_for :emails do |email| %>
<%= render 'emails_fields', f: email %>
<div class="links">
<%= link_to_add_association 'add email', f, :emails %>
</div>
<%= end %>
</div>
<%= f.button :submit %>
<% end %>
In your _emails_fields partial:
<div class="nested-fields">
<%= f.input :address %>
<%= link_to_remove_association "Remove email", f %>
</div>
Then setup cocoon's gem and javascript and you'll be good.
Reference: https://github.com/nathanvda/cocoon

Adding records to a 'has_many :through' association using a button

This is my first Rails app and have hit another wall. I have a User model and a Country model. They have a many-to-many relationship, which I join together with a Trip model.
A user can maintain a list of countries that they have been to. On the Country page, I want to have a simple bootstrap button so the current_user can add or remove the country to their list.
I am using a partial that looks like the below to at least render buttons on all the pages.
_add_remove_countries.html.erb
<% if #user.countries.exists?(#country.id) %>
<%= form_for(#user) do |f| %>
<%= f.submit "Remove Country", class: "btn btn-info" %>
<% end %>
<% else %>
<%= form_for(#user) do |f| %>
<%= f.submit "Add Country", class: "btn btn-info" %>
<% end %>
<% end %>
I have tried a few different things, with no luck so I have just reverted to the basic structure. I am currently using a form_for, however that is just what has worked best so far, I am not tied to that solution.
Below are my controllers if needed, I have not set up a Trips controller as I am only using it to join the User and Country Model (maybe I need to set one up?).
users_controller.rb
class UsersController < ApplicationController
def index
#users = User.all
end
def show
#user = User.find(params[:id])
#countries = Country.all
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
session[:user_id] = #user.id
redirect_to #user
else
render 'new'
end
end
def update
redirect_to user_path
end
private
def user_params
params.require(:user).permit(:username, :email, :password, :password_confirmation)
end
end
countries_controller.rb
class CountriesController < ApplicationController
before_action :require_user, only: [:index, :show]
def index
#countries = Country.all
#sort = CS.countries.sort_by {|key, value| value}
#sort = #sort.first #sort.size - 2
end
def show
#country = Country.find(params[:id])
#user = User.find(session[:user_id])
end
end
my suggestion using collection_select (and click link in case you would like to know more about collection_select) to add countries to user while editing user, below is sample code to help (using edit method)
user_controller
class UsersController < ApplicationController
def index
#users = User.all
end
def show
#user = User.find(params[:id])
#countries = Country.all
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
session[:user_id] = #user.id
redirect_to #user
else
render 'new'
end
end
# ---> here additional code to edit method
def edit
#user = User.find(params[:id])
#countries = Country.all
end
def update
#user = User.find(params[:id])
if #user.update_attributes(user_params)
redirect_to user_path
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:username,
:email,
:password,
:password_confirmation,
:country_ids => [])
# country_ids is an array that will save data for countries that user have been to
end
end
now this is the fun one, in your views\user\edit.html.erb
<%= form_for #user do |f| %>
<!-- simple -->
<p>Email : </p>
<p><%= f.text_field :email %></p>
<!-- if you using bootstrap -->
<div class="row form-group">
<%= f.label "email", :class => 'control-label col-sm-3' %>
<div class="col-sm-5">
<%= f.text_field :email, :class => 'form-control' %>
</div>
</div>
<!-- other inputs (password / password_confirmation) -->
<%= f.collection_select :country_ids, #countries, :id, :name, {}, { multiple: true, class: 'form-control' } %>
<% end %>

Rails Strong Params Issue With Nested Models

I am trying to render a new view on an already existing user show page. When trying to submit this view, I get param is missing or the value is empty: user. To be clear this is a skill partial being rendered on the user show page. For some reason it is using the strong params in my User Controller.
The code:
show.html.erb for user
<h4>Create a Skill</h4>
<%= render partial: "skills/form" %>
userscontroller.rb
def show
#user = User.find(params[:id])
#skill = Skill.new
#skills = #user.skills.all
end
private
def user_params
params.require(:user).permit(:username, :password, :avatar_url, :email, :about, :cover_letter, :city, :state)
end
end
SkillsController.rb
class SkillsController < ActionController::Base
def new
user = User.find(params[:user_id])
#skill = user.skills.new
end
def create
user = User.find(params[:user_id])
#skill = user.skills.new(skill_params)
if #skill.save
flash[:message] = "#{#skill.name} skill has been created!"
redirect_to user_path(user)
else
redirect_to new_user_skill_path
end
end
private
def skill_params
params.require(:skill).permit(:name, :level)
end
end
Also, I have Namespaced skills within user. No authentication in place yet.
EDIT: #nickm, here are the contents of skills/_form
<%= simple_form_for(Skill.new, :url => { :action => "create" }) do |f| %>
<%= f.input :name, label: 'Skill Name ' %>
<%= f.input :level, label: "Skill Level ", collection: ["Beginner","Proficient", "Intermediate", "Advanced", "Expert"], include_blank: false, include_hidden: false %>
<%= f.submit %>
<% end %>
The problem is that you aren't passing a user_id through the form. You would have to either add a form input:
<%= f.hidden_field :user_id, some_value %>
Then find the user:
user = User.find(params[:skill][:user_id])
and then make skill_params
def skill_params
params.require(:skill).permit(:name, :level, user_id)
end
Or optionally, set the value of user_id in your controller action. Not sure how you're going to pass that value since you haven't built any authentication yet. If you were using something like devise you could do
current_user.skills.new(skills_params)
...in your create action.

Trying to have 2 forms pass to 2 different controllers from one view

I have 2 forms in one view one is displayed if the user is a moderator and the other if it is a normal user and they both send the information to 2 different controllers. My problem is that if its a normal user, the form that is displayed for them uses the wrong controller.
Here is the coding
categories/new.html.erb
<% if current_user.mod_of_game? #guide %>
<%= form_for([#guide, #category], url: guide_categories_path) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name, "Category name" %>
<%= f.text_field :name %>
<%= f.submit "Next" %>
<% end %>
<% else %>
<%= form_for([#guide, #check_category], url: check_category_post_path) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name, "Category name" %>
<%= f.text_field :name %>
<%= f.submit "Next" %>
<% end %>
<% end %>
Categories controller
before_action :mod_checker, only: [:create]
def new
#guide = Guide.friendly.find(params[:guide_id])
#category = Guide.friendly.find(#guide.id).categories.new
#check_category = CheckCategory.new
end
def create
#guide = Guide.friendly.find(params[:guide_id])
#category = Guide.friendly.find(#guide.id).categories.new(category_params)
if ((#category.save) && (current_user.mod_of_game? #guide))
flash[:info] = "guide category added succesfully!"
redirect_to #guide
else
render 'new'
end
end
private
def category_params
params.require(:category).permit(:name)
end
def mod_checker
#guide = Guide.friendly.find(params[:guide_id])
unless current_user.mod_of_game? #guide
flash[:danger] = "Sorry something went wrong!"
redirect_to root_path
end
end
check_categories controller
def new
end
def create
if #check_category.save
flash[:info] = "Game category added successfully. A mod will apporve it shortly."
redirect_to #guide
else
render 'new'
end
end
private
def check_category_params
params.require(:check_category).permit(:name)
end
and the routes
resources :guides do
resources :categories, only: [:new, :create, :edit, :update]
end
resources :check_categories, only: [:new, :edit, :update]
match 'guides/:guide_id/categories/' => 'check_categories#create', :via => :post, as: :check_category_post
sorry the coding is a bit messy, the 4 spaces to put it in a code block was spacing my coding weird.
When i have a non moderator user submit the form, the before action in the categories controller is run and I'm redirected to the homepage. I don't know why it does this because the submit path should go to the check_categories controller for non moderator users, the check_categories controller doesn't have the before filter.
Why does it use the before filter in the controller I'm not using for that form? How can I fix it?
Building this app to learn rails better. So I can only assume lack of rails knowledge is causing me to do something wrong.
Bad practice to have two forms with identical code (apart from the path) - goes against DRY Don't Repeat Yourself.
As mentioned by #Akash, this sounds like a job for authorization.
Further, it also denotes that you have issues with the underlying structure of your code. Specifically, you have an antipattern with CheckCategory (you can put it all into the Category model):
#config/routes.rb
resources :guides do
resources :categories, only: [:new, :create, :edit, :update] do
patch :approve, on: :member
end
end
#app/models/category.rb
class Category < ActiveRecord::Base
before_action :set_guide
def new
#category = current_user.categories.new
flash[:notice] = "Since you are not a moderator, this will have to be approved." unless current_user.mod_of_game? #guide
end
def create
#category = current_user.categories.new category_params
#category.guide = #guide
#category.save
end
def approve
#category = #guide.categories.find params[:id]
#category.approve
end
private
def set_guide
#guide = Guide.find params[:guide_id]
end
end
#app/views/categories/new.html.erb
<%= form_for [#guide, #category] do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name, "Category name" %>
<%= f.text_field :name %>
<%= f.submit "Next" %>
<% end %>
The above will solve most of your structural issues.
--
To fix the authorization issue, you'll be best denoting whether the category is "approved" in the model:
#app/models/category.rb
class Category < ActiveRecord::Base
enum status: [:pending, :approved]
belongs_to :user
belongs_to :guide
validates :user, :guide presence: true
before_create :set_status
def approve
self.update status: "approved"
end
private
def set_status
self[:status] = "approved" if self.user.mod_of_game? self.guide
end
end
--
If I understand correctly, you want to allow anyone to create a category, but none-mods are to have their categories "checked" by a moderator.
The code above should implement this for you.
You will need to add a gem such as CanCan CanCanCan to implement some authorization:
#app/views/categories/index.html.erb
<% #categories.each do |category| %>
<%= link_to "Approve", guide_category_approve_path(#guide, category) if category.waiting? && can? :update, Category %>
<% end %>
Use "Cancan" Gem and give authorization

Cannot enter simply form information into SQLite DB (Rails)

So, I'm running into a fairly simple problem, where I cannot enter some simple form values into my SQLite DB (Rails).
Interestingly, the code doesn't fail - I submit the form, and am redirected successfully to the correct URL - and a record IS inserted into the DB, but the columns "name, type and user_id" are not filled with anything. To clarify, the columns are blank, for that new record.
If I comment out the code in the "create" and simply spit out the params (render plain: params[:plan].inspect) I do see all the correct parameters filled out, so I have a feeling there must be something wrong in the line:
#plan = Plan.new(params[:plan])
I'm really stuck here, any guidance would be much appreciated!
The create form
<h1> Create a new plan </h1>
<%= form_for :plan, url: plans_path do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %>
</p>
<p>
<%= f.label :type %><br>
<%= f.text_field :type %>
</p>
<%= f.hidden_field :user_id, :value => current_user.id %>
<p>
<%= f.submit %>
</p>
<% end %>
plans_controller.rb
class PlansController < ApplicationController
def index
#plans = Plan.all
end
def show
#plan = Plan.find(params[:id])
end
def new
#plan = Plan.new
end
def create
#render plain: params[:plan].inspect
params.permit!
#plan = Plan.new(params[:plan])
if #plan.save
redirect_to #plan
else
redirect_to dashboard_path, :notice => "Plan NOT Created!"
end
end
end
The Model
class Plan < ActiveRecord::Base
end
Edit the plans_controller.rb:-
def create
#render plain: params[:plan].inspect
#plan = Plan.new(plan_params)
if #plan.save
redirect_to #plan
else
redirect_to dashboard_path, :notice => "Plan NOT Created!"
end
end
private
def plan_params
params.require(:plan).permit(:name,:type,:user_id)
end
Change the field name type as :-
rails g migration rename_fields_in_plans
class RenameFieldsInPlans < ActiveRecord::Migration
def change
rename_column :plans, :type, :plan_type
end
end
Then run the command:-
rake db:migrate

Resources