I had a working Rails5.2 application that needed a database restructure to improve the organisation of some tables. Since this change, I've found that some of my forms fails consistently on my local machine with this error:
ActionController::InvalidAuthenticityToken in PayBandsController#create
I've had to change a lot in the application as part of the refactor, of course, but the tests are all passing.
I can avoid the issue by setting skip_before_action :verify_authenticity_token on the relevant controller (per previous SO threads) but this isn't a good fix, of course. I'd like to know what the root cause is and eliminate it.
The form works when it's on its own page, placed with render in the HTML.
The form works when I set the skip_before_action.
The form fails with an AuthenticityToken error otherwise - even though the same structure worked before.
This is all tested on my local machine with a valid session.
The form is created within a service object as the final line of a data table (so that I can add new lines to the table). Is this a potential cause of the problem - it's rendered in the service object rather than in the ERB? Although this approach worked fine before the refactor...
_table_data += ApplicationController.render(partial: 'datasets/pay_band_numbers_form', locals:{ _dataset_id: _dataset.id, _namespace: _key })
The partial is as follows and, as I said, works if placed on a page by itself (e.g. pay_bands/new) but not where it needs to be (on dataset/edit):
<%= form_for (PayBand.new), namespace: _namespace do |f| %>
<%= f.hidden_field :dataset_id, :value => _dataset_id %>
<%= f.text_field :label, :tabindex => 11, :required => true %>
<%= f.number_field :number_women, :tabindex => 12, :required => true %>
<%= f.number_field :number_men, :tabindex => 13, :required => true %>
<%= f.submit "✔".html_safe, :tabindex => 14, class: "button green-button checkmark" %>
<% end %>
The models involved are Dataset and PayBand, which are defined as follows.
class Dataset < ApplicationRecord
belongs_to :organisation
has_many :pay_bands, inverse_of: :dataset, dependent: :destroy
accepts_nested_attributes_for :pay_bands
default_scope -> { order(created_at: :desc) }
validates :name, presence: true
validates :organisation_id, presence: true
end
class PayBand < ApplicationRecord
belongs_to :dataset
has_many :ages_salaries_genders, inverse_of: :pay_band, dependent: :destroy
validates :dataset_id, presence: true
validates :label, presence: true
end
The relevant parts of the dataset controller are:
class DatasetsController < ApplicationController
protect_from_forgery with: :exception
load_and_authorize_resource
before_action :authenticate_user!
.
.
.
def edit
#dataset = Dataset.find(params[:id])
end
def update
#dataset = Dataset.find(params[:id])
if #dataset.update_attributes(dataset_params) && dataset_params[:name]
flash[:notice] = "Survey updated"
redirect_back fallback_location: dataset_path(#dataset)
else
flash[:alert] = "Attempted update failed"
redirect_to edit_dataset_path(#dataset)
end
end
.
.
.
private
def dataset_params
params.require(:dataset).permit(:name)
end
end
The pay-band controller is:
class PayBandsController < ApplicationController
protect_from_forgery with: :exception
# skip_before_action :verify_authenticity_token #DANGEROUS!!!!!
load_and_authorize_resource
before_action :authenticate_user!
def create
#pay_band = PayBand.new(pay_band_params)
if #pay_band.save
flash[:notice] = "Data saved!"
redirect_to edit_dataset_path(Dataset.find(#pay_band.dataset_id))
else
flash[:alert] = "There was an error. Your data was not saved."
redirect_back fallback_location: edit_dataset_path(Dataset.find(pay_band_params[:dataset_id]))
end
end
private
def pay_band_params
params.require(:pay_band).permit(:label, :number_women, :number_men, :avg_salary_women, :avg_salary_men, :dataset_id)
end
end
All of the above controller structure is identical to the working version of the app, before I fiddled with the data models!
===
Edit
I decided to check the headers, after some more research, so see if I could see the token value. However, the token value in the meta csrf-token header tag doesn't match any of the 3 different tokens in the 3 different forms on this page. But one works anyway (the simple form that's just rendered in the ERB template) and the other two don't (the ones rendered in the service object). Which all leaves me just as confused as I was before!
Also, I tried removing Turbolinks from my GemFile and application.js file, re-running bundle and rails s, but it made no difference.
For future reference for anyone else having the same problem (AuthenticityToken errors for a form that's been built in a helper or service object), I moved the form directly into the view and rendered it there - and the problem disappeared. I've removed the skip_before_action :verify_authenticity_token and all is now well!
Related
baby Ruby coder here.
I followed the steps here:https://masteruby.github.io/weekly-rails/2014/08/05/how-to-add-voting-to-rails-app.html#.XMFebOhKg2w to upload this act_as_votable gem however when I refresh my site to see if it works I get the following error: undefined method `acts_as_votable' for #<Class:0x00007f65df881b38>
The code does not seem to like the fact that I have put act_as_votable in my model I would like to use it on.
The error in my console also indicates that something is wrong in my controller. Do I need to define something there too?
Thanks in advance,
Angela
Model I want to use the act_as_votable gem on, you can see i have added it as the instructions instructed:
class Hairstyle < ApplicationRecord
belongs_to :user, optional: true
has_many :comments, dependent: :destroy
validates :name, presence: true
validates :description, presence: true
validates :category, presence: true
acts_as_votable
mount_uploader :photo, PhotoUploader
end
My hairstyles controller with the 'upvote' method at the end:
class HairstylesController < ApplicationController
def index
if params[:category].present?
#hairstyles = Hairstyle.where(category: params[:category])
elsif params[:search].present?
#hairstyles = Hairstyle.where('name ILIKE ?', '%#{params[:search]}%')
else
#hairstyles = Hairstyle.all
end
end
def show
#hairstyle = Hairstyle.find(params[:id])
#comment = Comment.new
end
def new
#hairstyle = Hairstyle.new
end
def create
#hairstyle = Hairstyle.create(hairstyle_params)
#hairstyle.user = current_user
if #hairstyle.save!
redirect_to hairstyle_path(#hairstyle)
else
render 'new'
end
end
def edit
#hairstyle = Hairstyle.find(params[:id])
end
def update
#hairstyle = Hairstyle.find(params[:id])
#hairstyle.update(hairstyle_params)
redirect_to hairstyles_path
end
def destroy
#hairstyle = Hairstyle.find(params[:id])
#hairstyle.destroy
redirect_to hairstyles_path
end
def upvote
#hairstyle = Hairstyle.find(params[:id])
#hairstyle.upvote_by current_user
redirect_to hairstyles_path
end
private
def hairstyle_params
params.require(:hairstyle).permit(:name, :description, :category, :location, :stylist, :photo, :video_url, :photo_cache)
end
end
My index file i'd like to display the gem on:
<% #hairstyles.each do |hairstyle| %>
<%= link_to "upvote", like_hairstyle_path(hairstyle), method: :put%>
<%end %>
</div>
Here is my repo if needed :https://github.com/Angela-Inniss/hair-do
It looks like you didn't run all 4 setup steps:
add 'acts_as_votable' to gemfile
Then run from terminal:
bundle install
rails generate acts_as_votable:migration
rake db:migrate
I cloned & setup your repo & I saw a few things going on that might be the cause:
The original error I got in this clone was due to act_as_taggable on your models. Your models should be annotated as acts_as_taggable (plural)
When I was initially testing, I wasn't logged in. This silently fails, which makes it seem like upvote isn't work. You might want to disable those hearts unless a user is logged in
Your html/erb template has some commented out code and such. This might be causing the link/URL to get swallowed and be non-clickable. I resolved it by deleting all styling & formatting except the link I was testing. I like using Haml to help reduce these kinds of nesting errors
I didn't run into your class error, but I would suggest running spring stop and trying to start your server again (I disable spring on all of my rails projects)
I have two models Project and ProjectPipeline.
I want to create a Project form that also has fields from the ProjectPipeline model. I have created the form successfully but when I hit save the values aren't stored on the database.
project.rb
class Project < ActiveRecord::Base
has_one :project_pipeline
accepts_nested_attributes_for :project_pipeline
self.primary_key = :project_id
end
projectpipeline.rb
class ProjectPipeline < ActiveRecord::Base
belongs_to :project, autosave: :true
validates_uniqueness_of :project_id
end
I don't always want a project pipeline but under the right conditions based on a user viewing a project. I want the project pipeline fields to be built, but only saved if the user chooses to save/populate them.
So when the project is shown I build a temporary project pipeline using the project_id: from params[:id] (not sure if I really need to do this). Then when the project is saved I use the create_attributes. But if it has already been created or built I just want to let the has_one and belongs_to association kick in and then use update_attributes.
My issue is when I am trying to save, I am either hitting a 'Forbidden Attribute' error if I use params[:project_pipeline] or having nothing saved at all if I used project_params. I have checked and rechecked all my fields are in project_params and even tried using a project_pipeline_params but that didn't feel right.
It is driving me nuts and I need to sleep.
projects_controller.rb
def show
#project = Project.find(params[:id])
if #project.project_pipeline
else
#project.build_project_pipeline(project_id: params[:id])
end
autopopulate
end
def update
#project = Project.find(params[:id])
if #project.project_pipeline
else
#project.build_project_pipeline(project_id: params[:id], project_type: params[:project_pipeline][:project_type], project_stage: params[:project_pipeline][:project_stage])
end
if #project.update_attributes(project_params)
flash[:success] = "Project Updated"
redirect_to [#project]
else
render 'edit'
end
end
def project_params
params.require(:project).permit(:user_id, project_pipeline_attributes:[:project_id,:project_type,:project_stage,
:product_volume,:product_value,:project_status,:outcome, :_destroy])
end
show.html.haml
- provide(:title, "Show Project")
%h1= #project.project_title
= simple_form_for(#project) do |f|
= f.input :id, :as => :hidden, :value => #project, :readonly => true
= f.input :user_id, label: 'Assigned to Account Manager', :collection => #account_managers, :label_method => lambda { |r| "#{r.first_name} #{r.last_name}" }
= f.input :project_id, :readonly => true
= f.input :status, :readonly => true
= f.input :project_stage, :readonly => true
- if #project.project_codename = "project pipeline"
= simple_fields_for #project.project_pipeline do |i|
%h2 Project Pipeline
- if #project.user_id == current_user.id
= i.input :project_volume, label: 'Project Status', collection: #project_status
= i.input :project_value, label: 'Project Status', collection: #project_status
= i.input :project_status, label: 'Project Status', collection: #project_status
= i.input :outcome, label: 'Outcome', collection: #outcome
= f.submit 'Save'
If you've gotten this far I sincerely thank you.
Solution
You need to change few things here. Firstly:
= simple_fields_for #project.project_pipeline do |i|
When you pass the object, rails have no idea it is to be associated with the parent object and as a result will create a field named project[project_pipeline] instead of project[project_pipeline_attributes]. Instead you need to pass the association name and call this method on the form builder:
= f.simple_fields_for :project_pipeline do |i|
This will check find out that you have defined project_pipeline_attributes= method (using accept_nested_attributes_for` and will treat it as association. Then in your controller change your show action to:
def update
#project = Project.find(params[:id])
#project.assign_attributes(project_params)
if #project.save
flash[:success] = "Project Updated"
redirect_to #project
else
render 'edit'
end
end
And all should work. As a separate note, since you are allowing :_destroy attribute in nested params, I am assuming you want to be able to remove the record using nested attributes. If so, you need to add allow_destroy: true to your accepts_nested_attributes_for call.
Now a bit of styling:
You can improve your show action a bit. First of all, I've noticed you are building an empty pipeline in every single action if none has been declared yet. That mean that you probably should move this logic into your model:
class Project < AR::Base
after_initalize :add_pipeline
private
def add_pipeline
project_pipeline || build_project_pipeline
end
end
You also have the mysterious method prepopulate - most likely it should be model concern as well.
Another point: This syntax:
if something
else
# do sth
end
is somehow quite popular and makes the code unreadable as hell. Instead, use:
if !something
# do something
end
or (preferred)
unless something
# do something
end
I'm not sure from your description if this is the problem, but one the thing is that a update_attributes with a has_one, by default, will rebuild the children(!), so you would lose the attributes you initialised. You should provide de update_only: true option to accepts_nested_attributes_for.
You can find more on this here, in the rails docs. The line would be this:
accepts_nested_attributes_for :project_pipeline, update_only: true
Considering the after_initialize, that would result in every project always having a pipeline. While that could be desirable, it isn't necessarily, depending on your domain, so I'd be a bit careful with that.
Cheers,
Niels
I have a form that prompts for a job and job_files to attach. When the job save fails due to validation, the selected job_files disappear when the form redisplays. How do I retain the child fields? All parent fields are retained in the form but the child fields are gone.
job model:
class Job < ActiveRecord::Base
has_many :job_files, :dependent => :destroy
accepts_nested_attributes_for :job_files, :allow_destroy => true
validates_acceptance_of :disclaimer, :allow_nil => false, :accept => true, :on => :create, :message=>'Must accept Terms of Service'
end
job_files model:
class JobFile < ActiveRecord::Base
belongs_to :job
has_attached_file :file
end
jobs_controller:
def new
#upload_type = UploadType.find_by_id(params[:upload_job_id])
#job = Job.new
#job.startdate = Time.now
10.times {#job.job_files.build}
#global_settings = GlobalSettings.all
end
def create
#global_settings = GlobalSettings.all
if params[:cancel_button]
redirect_to root_path
else
#job = Job.new(params[:job])
#job.user_id = current_user.id
#upload_type = UploadType.find_by_id(#job.upload_type_id)
if #job.save
JobMailer.job_uploaded_notification(#upload_type,#job).deliver
flash[:notice] = "Job Created"
redirect_to root_path
else
# retain selected files if save fails
(10 - #job.job_files.size).times { #job.job_files.build }
flash[:error] = "Job Creation Failed"
render :action => :new
end
end
end
job_files partial within job form:
<%= form.fields_for :job_files, :html=>{ :multipart => true } do |f| %>
<li class="files_to_upload">
<%= f.file_field :file %>
</li>
<% end %>
Any help would be appreciated since I can not find any solution online.
That's standard-issue behavior. Really, the only way to workaround it would be to persist the files first, regardless of whether validation passes. Then, run your validations. But that's definitely a workaround.
You could also do client-side validations before the server-side ones; that way, you'd have better assurances that the user's data is valid BEFORE they submit to the server, such that they won't encounter any server-side validation failures, and the files will always make it. I'd probably go that route if I were in your shoes. Note that client-side validation is NOT a replacement for server-side validation, just will help your users in this case.
Hope that helps!
I'm currently working on a project using Rails 3.2 and Active Scaffold. I've created a simple controller for one of my models that is coded thusly:
class StudentsController < ApplicationController
before_filter :authenticate_user!
active_scaffold :student do |conf|
conf.label = "Students"
conf.columns = [:last_name, :first_name, :age, :gender, :grade_level, :current_grade]
conf.create.columns = [:last_name, :first_name, :age, :gender, :grade_level]
conf.update.columns = [:last_name, :first_name, :age, :gender, :grade_level]
conf.columns[:current_grade].actions_for_association_links = [:show]
conf.columns[:current_grade].sort_by :sql => "grade_level"
conf.actions = [:list, :search, :create, :update, :show, :delete]
list.columns.exclude :customer_id, :grade_level
list.sorting = {:last_name => 'ASC'}
end
def conditions_for_collection
["customer_id = #{current_user.customer_id}"]
end
def before_create_save(record)
record.customer_id = current_user.customer_id
end
end
My problem is this: When I delete a record, I receive a message that states '$record_name can't be deleted'. Yet I find the record is in fact deleted if I refresh the page. Upon examining my log file I see an error message stating:
undefined method `as_marked=' for #<Student:0x0000000554c1d0>
I tried adding :mark to my list of actions and that does solve the problem. However, I don't want a mark/checkbox column to show up in my list.
Any ideas? This is my first time using active scaffold, and I find this... annoying.
I've discovered that if I add this to my model:
def as_marked= (x)
end
it works, without showing a mark/checkbox column in my list.
For the record, I hate this solution :) If I come up with anything better I'll make sure to come back and update this answer.
I may just be missing something simple, but I am relatively inexperienced so it is likely. I've searched extensively for a solution without success.
I am using the fields_for function to build a nested form using the accepts_nested_attributes_for function. If the submit on the form fails the params are passed to the render of the new template only for the parent model. How do I pass the nested params for the child model so that fields that have been filled out previously remain filled. Note that I am using simple_form and HAML but I assume this shouldn't impact the solution greatly.
My models:
class Account < ActiveRecord::Base
attr_accessible :name
has_many :users, :dependent => :destroy
accepts_nested_attributes_for :users, :reject_if => proc { |a| a[:email].blank? }, :allow_destroy => true
end
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation
belongs_to :account
end
My accounts controller:
def new
#account = Account.new
#account.users.build
end
def create
#account = Account.new(params[:account])
if #account.save
flash[:success] = "Welcome."
redirect_to #account
else
#account.users.build
<- I suspect I need something here but unsure what
render :new
end
end
The key part of the accounts/new view:
= simple_form_for #account do |f|
= f.input :name
= f.simple_fields_for :users do |u|
= u.input :email
= u.input :password
= u.input :password_confirmation
= f.button :submit, :value => "Sign up"
My params on a failed save are:
:account {"name"=>"In", "users_attributes"=>{"0"=>{"email"=>"u#e.com", "password"=>"pass", "password_confirmation"=>"pass"}}}
As you can see, the key information, in the users_attributes section, is stored but I can't seem to have the email address default into the new form. Account name on the other hand is filled automatically as per Rails standard. I'm not sure if the solution should live in the accounts controller or in the accounts/new view, and have not had any luck with either.
Answers with .erb are, of course, fine.
I'm fairly new to Ruby and Rails so any assistance would be much appreciated.
The problem lies with attr_accessible, which designates the only attributes allowed for mass assignment.
I feel a bit silly in that I actually stated the problem in a comment last night and failed to notice:
accepts_nested_attributes_for :users will add a users_attributes= writer to the account to update the account's users.
This is true, but with attr_accessible :name, you've precluded every attribute but name being mass-assigned, users_attributes= included. So when you build a new account via Account.new(params[:account]), the users_attributes passed along in params are thrown away.
If you check the log you might note this warning:
WARNING: Can't mass-assign protected attributes: users_attributes
You can solve your original problem by adding :users_attributes to the attr_accessible call in the account class, allowing it to be mass-assigned.
Amazingly, after reading a blog post this evening, and some more trial and error, I worked this out myself.
You need to assign an #user variable in the 'new' action so that the user params are available for use in the 'create' action. You then need to use both the #account and #user variables in the view.
The changes look like this.
Accounts Controller:
def new
#account = Account.new
#user = #account.users.build
end
def create
#account = Account.new(params[:account])
#user = #account.users.build(params[:account][:user]
if #account.save
flash[:success] = "Welcome."
redirect_to #account
else
render :new
end
end
The accounts/new view changes to:
= simple_form_for #account do |f|
= f.input :name
= f.simple_fields_for [#account, #user] do |u|
= u.input :email
= u.input :password
= u.input :password_confirmation
= f.button :submit, :value => "Sign up"
In this case the params remain nested but have the user component explicitly defined:
:account {"name"=>"In", "user"=>{"email"=>"user#example.com", "password"=>"pass", "password_confirmation"=>"pass"}}
It has the additional side effect of removing the #account.users.build from within the else path as #numbers1311407 suggested
I am not certain whether their are other implications of this solution, I will need to work through it in the next few days, but for now I get the information I want defaulted into the view in the case of a failed create action.
#Beerlington and #numbers1311407 I appreciate the help in guiding me to the solution.