I have two objects, Course and TeacherRight. I create TeacherRight with a from placed on the Course 'show' view. I pass course_id to the TeacherRight controller as a hidden field:
=form_for(#right) do |f|
=hidden_field_tag :course_id, #course.id
%ul
%li=f.collection_select :user_id, User.order(:name)-#course.teachers, :id, :name, include_blank: false
%li=f.submit
The controller:
class TeacherRightsController < ApplicationController
def create
course = Course.find(params[:course_id])
#teacher_right = course.teacher_rights.build(teacher_right_params)
#teacher_right.save
redirect_to course
end
private
def teacher_right_params
params.require(:teacher_right).permit(:user_id)
end
#only owner can add teachers to course
def can_edit(course)
redirect_to root_url unless current_user == course.owner
end
It works fine, but I can't write a test for it! I wrote
class TeacherRightsControllerTest < ActionController::TestCase
def setup
#owner_1 = users(:owner_1)
#course = courses(:course)
#teacher_1 = users(:teacher_1)
end
test "teacher can't add teachers" do
log_in_as(#teacher_1)
assert_no_difference('TeacherRight.count') do
post :create, {teacher_right: {course_id:#course.id, user_id: #teacher_1.id }}
end
end
But it gives an error:
ActiveRecord::RecordNotFound: Couldn't find Course with 'id'=
app/controllers/teacher_rights_controller.rb:4:in `create'
As I can see,
post :create, {teacher_right: {course_id:#course.id, user_id: #teacher_2.id, }}
doesn't pass course_id in a way the controller can consume. How can I correct it?
You accidentally are posting the :course_id inside the :teacher_right hash, so it will show up as params[:teacher_right][:course_id] instead of the desired params[:course_id].
Your POST request should look like this:
post :create, {
course_id: #course.id,
teacher_right: { user_id: #teacher_1.id }
}
Related
In a controller I have an update method which creates a record (call it book), associates it to an existing record (call it author) and saves it.
Book belongs to one Author
add_author_to_book_controller.rb
def update
#author = App::Models::Author.new(params)
#book = App::Models::Book.where(id: params[:book_id]).first
#book.author = #author
#book.save!
# this works fine...
# puts #book.author.inspect
render json: { status: :ok }
end
add_author_to_book_controller_spec.rb
describe App::AddAuthorToBookController do
describe '#update' do
# this is a contrived example, there is more setup regarding creating the "book" properly...
let(:name) { 'foobar' }
let(:action) { xhr :put, :update, params }
let(:params) { { first_name: name } }
subject { book }
before { action }
it { expect(response.status).to eq 200 }
it 'should save the author to the book' do
# why is author nil here?
# puts book.author.inspect
expect(book.author.first_name).to eq name
end
end
end
I tried book.reload in the test but that didn't work. I'm new to rails, is there some conventional way of testing an associated record in a controller test?
Wasn't saving author before associating it to book...
def update
#author = App::Models::Author.new(params)
# was simply missing this
#author.save!
#book = App::Models::Book.where(id: params[:book_id]).first
#book.author = #author
#book.save!
# this works fine...
# puts #book.author.inspect
render json: { status: :ok }
end
First of all I recommend you to make your controllers more general because that is the correct architecture you need to follow, so your controller can be called authors_controller.rb and manage all authors stuff or books_controller.rb and manage all books stuff. And following this approach you can have a method associate_book which receives an author and a book and creates the right association. Let me explain it with code:
class Author < ApplicationRecord
has_many :books
# Fields :name
validates :name, presence: true
end
class Book < AoplicationRecord
# Optional because I think you want to add the author after create it
belongs_to :author, optional: true
# Fields :title, :publish_year, :author_id
validates :title, :publish_year, :author_id, presence: true
end
class AuthorsController < ApplicationController
def associate_book
# params here will contain the following [:author_id, book_id]
author = Author.find(params[:author_id])
book = Book.find(params[:book_id])
book.author = author
book.save!
rescue ActiveRecord::RecordInvalid => error
# This will allow you to catch exceptions related to the update
end
end
Then you can test this method by doing the following, supposing that this method will be called from a route
# Testing with RSpec
# spec/controllers/authors_controller.rb
RSpec.describe AuthorsController do
let(:author) { Author.first }
let(:book) { Book.first }
it 'Should associate an author with a provided book' do
expect do
post :associate_book, params: { author_id: author.id, book_id: book.id }
end.to change { author.books.count }.by(1)
end
end
This will check the total count of books associated to the author.
I'm doing simple Reddit like site. I'm trying to add button to reporting posts. I create report model, using button_to i try to post data to report controller to create it but i received NoMethodError in ReportsController#create undefined method merge' for "post_id":String
model/report.rb
class Report < ApplicationRecord
belongs_to :reporting_user, class_name: 'Author'
has_one :post
end
report_controller.rb
class ReportsController < ApplicationController
def create
report = Report.new(report_params)
flash[:notice] = if report.save
'Raported'
else
report.errors.full_messages.join('. ')
end
end
def report_params
params.require(:post).merge(reporting_user: current_author.id)
end
end
and button in view
= button_to "Report", reports_path, method: :post, params: {post: post}
What cause that problem?
edit:
params
=> <ActionController::Parameters {"authenticity_token"=>"sX0DQfM0rp97q8i16LGZfXPoSJNx15Hk4mmP35uFVh52bzVa30ei/Bxk/Bm40gnFmd2NvFEqj+Wze8ted66kig==", "post"=>"1", "controller"=>"reports", "action"=>"create"} permitted: false>
To start with you want to use belongs_to and not has_one.
class Report < ApplicationRecord
belongs_to :reporting_user, class_name: 'Author'
belongs_to :post
end
This correctly places the post_id foreign key column on reports. Using has_one places the fk column on posts which won't work.
And a generally superior solution would be to make reports a nested resource:
# /config/routes.rb
resources :posts do
resources :reports, only: [:create]
end
# app/controller/reports_controller.rb
class ReportsController
before_action :set_post
# POST /posts/:post_id/reports
def create
#report = #post.reports.new(reporting_user: current_author)
if #report.save
flash[:notice] = 'Reported'
else
flash[:notice] = report.errors.full_messages.join('. ')
end
redirect_to #post
end
private
def set_post
#post = Post.find(params[:post_id])
end
end
This lets you simplify the button to just:
= button_to "Report", post_reports_path(post), method: :post
Since the post_id is part of the path we don't need to send any additional params.
If you do want to let the user pass additional info through a form in the future a better way to create/update resources with params and session data is by passing a block:
#report = #post.reports.new(report_params) do |r|
r.reporting_user = current_user
end
ActionController::Parameters#require returns the value of the required key in the params. Usually this would be an object passed back from a form. In this example require would return {name: "Francesco", age: 22, role: "admin"} and merge would work.
Your view is sending back parameters that Rails is formatting into {post: 'string'}. We would need to see your view code to determine what exactly needs to change.
Update: From the new code posted we can see that the parameter sent back is "post"=>"1". Normally we would be expecting post: {id: 1, ...}.
Update: The button in the view would need the params key updated to something ala params: {post: {id: post.id}} EDIT: I agree that params: {report: { post_id: post}} is a better format.
The problem seems to be in report_params. When you call params.require(:post), it fetches :post from params -> the result is string. And you are calling merge on this string.
I'd recommend change in view:
= button_to "Report", reports_path, method: :post, params: { report: { post_id: post} }
then in controller:
def report_params
params.require(:report).permit(:post_id).merge(reporting_user_id: current_author.id)
end
Note, that I changed also the naming according to conventions: model_id for id of the model, model or model itself.
I have two models. First is Taxirecord and second is Carpark. Each Taxirecord may have its own Carpark. I have a problem with passing taxirecord_id to Carpark record. I have route
car_new GET /taxidetail/:taxirecord_id/carpark/new(.:format) carparks#new
And i want to pass :taxirecord_id, which is id of taxirecord that im editing, to my create controller.
My carpark model:
class Carpark < ActiveRecord::Base
belongs_to :taxirecord
end
In controller im finding taxirecord_id by find function based on param :taxirecord_id, but id is nil when create is called. Can you please help me to find out what Im doing wrong and how Can I solve this problem? Thanks for any help!
My carpark controller
class CarparksController < ApplicationController
def new
#car = Carpark.new
end
def create
#car = Carpark.new(carpark_params, taxirecord_id: Taxirecord.find(params[:taxirecord_id]))
if #car.save
flash[:notice] = "Zaznam byl ulozen"
redirect_to root_path
else
flash[:notice] = "Zaznam nebyl ulozen"
render 'new'
end
end
private def carpark_params
params.require(:carpark).permit(:car_brand, :car_type, :driver_name, :driver_tel)
end
end
I finally get it work
Ive added <%=link_to 'New Carpark', {:controller => "carparks", :action => "new", :taxirecord_id => #taxi_record.id }%>
to my taxirecord form and to carpark form <%= hidden_field_tag :taxirecord_id, params[:taxirecord_id] %>
And to my carpark controller : #carpark.taxirecord_id = params[:taxirecord_id]
Thanks everyone for great support and help!
I'd lean towards using something like:
before_action :assign_taxirecord
...
private
def assign_taxirecord
#taxirecord = TaxiRecord.find(params[:taxirecord_id])
end
And then in the create action:
def create
#car = #taxirecord.build_carpark(carpark_params)
...
end
Obviously there's a little tailoring needed to your requirements (i.e. for what actions the before_action is called), but I hope that helps!
No need to send taxirecord id.
class Carpark < ApplicationRecord
belongs_to :taxirecord
end
class Taxirecord < ApplicationRecord
has_one :carpark
end
Rails.application.routes.draw do
resources :taxirecords do
resources :carparks
end
end
for new taxirecord
t = Taxirecord.new(:registration => "efgh", :description =>"test")
for new carpark
t.create_carpark(:description=>"abcd")
#=> #<Carpark id: 2, taxirecord_id: 2, description: "abcd", created_at: "2017-10-12 10:55:38", updated_at: "2017-10-12 10:55:38">
I am battling an error with nested attributes and trying to fix the cop error at the same time. So here is the walk through. A coupon code may be submitted with the form using nested attributes that may affect the price of the job. This only occurs if the coupon code is valid. In this scenario the coupon code has already been assigned so the first if coupon_code && coupon.nil? is triggered. When the form comes back around the flash message works correctly but simple form does not display the value. I could adjust simple form to have the value with an instance variable but I'm starting to smell something a bit off here in my logic. Also, the smell of Assignment Branch Condition is starting to worry me. I can move forward with this, but the user would like to see the code. I would too.
Cop Error:
app/controllers/payments_controller.rb:9:3: C: Assignment Branch Condition size for update is too high. [17.97/15]
Controller:
class PaymentsController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :route_not_found_error
Numeric.include CoreExtensions::Numeric::Percentage
def update
#job = Job.find(params[:job_id])
coupon_code = params[:job][:coupon_attributes][:code]
coupon = validate_coupon(coupon_code)
if coupon_code && coupon.nil?
#coupon_code = coupon_code
flash.now[:error] = t('flash_messages.coupons.id.not_found')
render 'payments/new', layout: 'nested/job/payment'
else
update_job(#job, coupon)
update_coupon(coupon, #job) if coupon
redirect_to #job.vanity_url
end
end
def new
#job = Job.find(params[:job_id])
return if reroute?(#job)
render 'payments/new', layout: 'nested/job/payment'
end
private
def update_job(job, coupon)
job.start_at = DateTime.now
job.end_at = AppConfig.product['settings']['job_active_for_day_num'].days.from_now
job.paid_at = DateTime.now
job.price = price_job(coupon)
# job.save
end
def validate_coupon(coupon_code)
return nil unless coupon_code.present?
coupon = Coupon.active.find_by_code(coupon_code)
return nil unless coupon.present?
coupon
end
def price_job(coupon)
price = AppConfig.product['settings']['job_base_price']
return price unless coupon
price = coupon.percent_discount.percent_of(price)
price
end
def update_coupon(coupon, job)
coupon.job_id = job.id
coupon.executed_at = DateTime.now
coupon.save
end
end
View:
ruby:
content_for :body_id_class, 'PaymentNew'
content_for :js_instance, 'viewPaymentNew'
content_for :browser_title, 'Payment'
job_base_price = AppConfig.product['settings']['job_base_price']
coupon_code = #coupon_code ||= ''
= simple_form_for(#job, url: job_payment_path, html: { id: 'payment-processor-form' }) do |j|
div[class='row']
div[class='col-md-12']
div[class='panel panel-default']
div[class='panel-heading']
h3[class='panel-title']
|Total Cost
div[class='panel-body']
h2[class='job-cost' data-initial = "#{job_base_price}"]
= number_to_currency(job_base_price)
div[class='panel-heading']
h3[class='panel-title']
|Have a coupon?
div[class='panel-body']
div[class='row-inline']
div[class='row-block row-block-one']
= j.simple_fields_for :coupon_attributes, #job.coupon do |c|
= c.input_field :code, maxlength: 50, id: 'coupon-code', class: 'form-control', data: { 'initial' => 0 }, value: coupon_code
div[class='row-block']
button[type='button' class='btn btn-primary' id='coupon-verify' ]
|Verify
p[class='help-hint']
= t('simple_form.hints.coupon.code')
div[class='row']
div[class='col-md-12']
= j.button :button, type: 'button', class: 'btn-primary text-uppercase', id: 'purchase-job' do
= job_posting_button_step_label
Updates
Refactoring this code to work with the post below. Factories fixed factorygirl create model association NoMethodError: undefined method
You have quite a few code smells going on in that fat old controller.
Most of them seem to be symtoms that all is not well on the model layer and that you are not modeling the domain very well.
You might want to consider something like this:
class Job < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :job
belongs_to :coupon
end
class Coupon < ActiveRecord::Base
validates_uniqueness_of :code
end
This will let our countroller focus on CRUD'ing a single resouce rather than trying to herd a bunch of cats.
So lets look at enforcing the business logic for coupons.
class Payment < ActiveRecord::Base
belongs_to :job
belongs_to :coupon
validate :coupon_must_be_active
attr_writer :coupon_code
def coupon_code=(code)
coupon = Coupon.find_by(code: code)
#coupon_code = code
end
private
def coupon_must_be_active
if coupon
errors[:coupon] << "must be active." unless coupon.active?
elsif #coupon_code.present?
errors[:coupon_code] << "is not valid."
end
end
end
The custom attribute writer loads the coupon from the a code. The validation sets up our business logic rules.
We really should do the same when it comes to the job pricing:
class Job < ActiveRecord::Base
after_initialize :set_price
def set_price
self.price ||= AppConfig.product['settings']['job_base_price']
end
end
class Payment < ActiveRecord::Base
after_initialize :set_price
validates_presence_of :job
def net_price
return job.price unless coupon
job.price * (coupon.percent_discount * 00.1)
end
# ...
end
We can then write our controller like so:
class PaymentsController
before_action :set_job
# GET /jobs/:job_id/payments/new
def new
#payment = #job.payments.new
end
# POST /jobs/:job_id/payments
def create
#payment = #job.payments.create(payment_params)
end
# PATCH /jobs/:job_id/payments/:id
def update
#payment = #job.payments.find(params[:id])
end
private
def set_job
#job = Job.find(params[:job_id])
end
def payment_params
params.require(:payment)
.permit(:coupon_code)
end
end
We can then simply setup the form with:
= simple_form_for([#job, #payment]) do |f|
= f.input :coupon_code
= f.submit
Note that you don't want to take the price from the user unless you intend to implement the honor system - you should get it from your models by setting up association callbacks.
When I write a message and when pressing the send option,
I want to store student_id, coach_id and message to the database. student_id and coach_id are being saved, but the message field is not being saved. It shows null in the database. How do I fix this?
Any help is appreciated.
Controller file:
class CourseQueriesController <ApplicationController
def index
#course_query = CourseQuery.new
end
def create
# #course_query = CourseQuery.new(course_query_params)
#course_query = CourseQuery.where(student_id: current_student.id, coach_id: "2", message: params[:message]).first_or_create
if #course_query.save
redirect_to course_queries_path, notice: 'Query was successfully send.'
else
render :new
end
end
private
def set_course_query
#course_query = CourseQuery.find(params[:id])
end
# def course_query_params
# params[:course_query].permit(:message)
# end
end
model/course_query.rb:
class CourseQuery < ActiveRecord::Base
belongs_to :student
belongs_to :coach
end
view/course_query/index.html.erb:
<%= simple_form_for (#course_query) do |f| %>
<%= f.button :submit , "Send or press enter"%>
<%= f.input :message %>
<% end %>
database /course_queries:
It seems you didn't permit :course_query.
Try to permit your params the following way:
def course_query_params
params.require(:course_query).permit(:message)
end
But according to the 2nd way you pass params (params[:message]) I think you have a bit different params structure. So try another one:
def course_query_params
params.permit(:message)
end
When you look into the params generated in the log, you will see that the message inside the course_query hash, so params[:message] should be params[:course_query][:message]
#course_query = CourseQuery.where(student_id: current_student.id, coach_id: "2", message: params[:course_query][:message]).first_or_create