Rails unable to get before_destroy to call when deleting - ruby-on-rails

I've been at this for a while and need a smarter person's opinion. I'm trying to check the passcode I have in my database matches the passcode the user enters in order to delete a record. I believe that I should be doing this in the model, and I am refactoring to use the before_destroy method. However I can't even get the before_destroy method to execute when I click on the delete button I made. The controller does execute the destroy method though.
View
<%= button_to('Destroy', #dish, method: "delete") %>
Model - the puts passcode_check? is never called from what I see
class Dish < ActiveRecord::Base
before_destroy :passcode_check?
validates :username, presence: true
validates :passcode, presence: true
validates :guests, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
private
def passcode_check?
puts "passcode_check?"
if #dish.passcode != params[:pass]
#dish.errors.add( :base, 'Unable to delete record; Reason: Passcodes did not match.')
return false
else
#dish.errors.add( :base, 'test.')
return false
end
end
end
Controller - this method is influx as I want to validate in the model
def destroy
if #dish.passcode == params[:pass]
#dish.destroy unless #dish.errors
respond_to do |format|
format.html { redirect_to dishes_url, notice: 'Record delete.' }
format.json { head :no_content }
end
else
respond_to do |format|
format.html { redirect_to #dish, action: 'show', notice: 'Unable to delete record; Reason: Passcodes did not match.' }
format.json { render json: #dish.errors, status: :unprocessable_entity }
end
end
end

In your controller you're using #dish.errors which will always return an ActiveModel::Errors object and therefore be truthy. So the unless #dish.errors statement modifier never lets #dish.destroy run and consequently neither will your callback. Change it to:
#dish.destroy if #dish.errors.empty?
And that should be it. Although it doesn't make much sense to check for errors yet since no validations have even run. Just call #dish.destroy and let your before_destroy callback halt the deletion by returning false and, conversely, let the deletion happen by returning true.

Related

How to fix Error ActiveRecord::RecordNotSaved when calling ActiveRecord#find_or_create_by

I am working on a website and whenever someone forgets a null field in the form I get an error saying
You cannot call create unless the parent is saved
This is the trace:
Application Trace
app/views/technicians/offer_comments/_offer_comment.html.slim:1:in `_app_views_technicians_offer_comments__offer_comment_html_slim__1148413763742950523_70319840794240'
app/views/offer_comments/index.html.slim:2:in `_app_views_offer_comments_index_html_slim___917297770217289302_70319839456700'
app/views/shared/offers/_comments.html.slim:8:in `_app_views_shared_offers__comments_html_slim__3779418887197040636_70319839163900'
app/views/technicians/auctions/show.html.slim:98:in `block in _app_views_technicians_auctions_show_html_slim___1454306351028108654_70319829646100'
app/helpers/application_helper.rb:14:in `rescue in cache'
app/helpers/application_helper.rb:6:in `cache'
app/views/technicians/auctions/show.html.slim:1:in `_app_views_technicians_auctions_show_html_slim___1454306351028108654_70319829646100'
app/controllers/technicians/offers_controller.rb:54:in `update'
The error appears in the first line of this html.slim view:
- offer_comment.read_receipts.find_or_create_by user: current_user
.comment id="offer-#{offer_comment.offer_id}-comment-#{offer_comment.id}"
.by
- if offer_comment.user == current_user
= t ".you"
- else
= t ".not_you"
= " - "
= t '.date', date: time_ago_in_words(offer_comment.created_at)
.content
= raw markdown offer_comment.content
The interesting part is that this error only occurs when I call another object, offers, in the main view in which the previous code is rendered: show.html.slim (last line)
ul#customer-auction-tabs.tabs.clean.collapse(data-tabs)
a#auctions-tabs-chevron href="#"
i#auctions-tabs-chevron-icon.fas.fa-chevron-up
li.tabs-title class=chat_active_class
a#chat-tab href="#chat" aria-selected="true"= t '.tabs.chat'
li.tabs-title class=offer_active_class
a#offers-tab href="#offers"= t '.tabs.offer'
- if comments_count > 0
li.tabs-title class=comments_active_class
a#comments-tab href="#comments"= t '.tabs.comments'
li.tabs-title class=other_active_class
a#other-tab href="#other"= t '.tabs.other'
.auctions.tabs-content data-tabs-content="customer-auction-tabs"
#chat.tabs-panel class=chat_active_class
= render partial: "shared/auctions/chat", locals: { auction: auction }
#offers.tabs-panel class=offer_active_class
= render partial: "shared/offers/new", locals: { offer: offer }
#comments.tabs-panel class=comments_active_class
= render partial: 'shared/offers/comments', locals: { offer: offer }
#other.tabs-panel class=other_active_class
- if auction.offers.count.zero?
= t "ingen andre bud endnu"
= render "shared/offers/other"
.offers= render offers
I don't understand how this works because find_or_create_by is apparently supposed to work even if the object hasn't been saved.
Can someone help me solve this issue, and preferably avoid using logic like find_or_create_by in the view at all?
Here is part of the Offer model:
class Offer < ApplicationRecord
has_paper_trail
belongs_to :auction, -> { with_deleted }, counter_cache: true, touch: true
belongs_to :technician, counter_cache: true, foreign_key: :technician_id
has_one :settings, through: :technician
has_many :comments, -> { order(created_at: :asc) }, class_name: "OfferComment"
has_one :review, as: :target
delegate :rating, to: :review, allow_nil: true
delegate :rating_date, to: :review, allow_nil: true
delegate :rating_comment, to: :review, allow_nil: true
validates :description, presence: true
validates :cents, presence: true, numericality: { only_integer: true, greater_than: 0 }
validate :amount_validity
scope :not_by, ->(technician) { where.not(technician: technician) }
Here is also the controller update action that gets called when updating the form with a null field:
class Technicians::OffersController < Technicians::ApplicationController
rescue_from ActiveRecord::RecordNotFound do
render "technicians/auctions/lost", status: 404
end
def update
offer.attributes = offer_params
changed = offer.changed?
if offer.save
OfferUpdatedWorker.perform_async offer.id if changed
flash[:info] = t(".success")
redirect_to [:technicians, auction]
else
flash.now[:error] = t(".failure")
render "technicians/auctions/show",
locals: { auction: auction, offer: offer },
status: 400
end
end
Another important file to note is the auction controller that originally calls "technicians/auctions/show"
def show
render(:lost) && return if lost?
render :show, locals: {
offers: sorted_o,
auction: auction,
#other: other,
offer: offer,
} if stale? **cache_options(auction.id, auction.updated_at)
end
private
#=begin
def sorted_o
#sorted_o ||= begin
field = (%w[cheapest closest guarantee] & [params[:sort]])[0].presence || "cheapest"
case field
when "closest"
auction
.offers
.includes(:auction, :technician, :review)
.sort_by { |o| distance(o.technician, auction) }
when "guarantee"
auction
.offers
.includes(:auction, :technician, :review)
.joins(:settings)
.order("technician_settings.guarantee desc")
else
auction
.offers
.includes(:auction, :technician, :review)
.order(cents: :asc)
end
end
end
#=end
def offer
#offer ||= auction.offers.by(current_user) ||
auction.offers.new(technician: current_user)
end
It looks like you need offer_comment to be saved before a read_receipt that belongs to it can be created, which makes sense - the offer_comment doesn't have an id until it has been saved.
This might get you past the problem.
offer_comment.tap(&:save).read_receipts.find_or_create_by user: current_user
I fixed it. The cause of the problem was that when the update action fails in offers_controller.rb it calls the show view without the offers variable, this variable is somehow related to the offer_comments, but I'm not sure how/why because offer_comments is supposed to be only related to the variable offer and not offers.
However, when I checked the console there were null elements in the offer_comments so I just went on and changed the view to only show non null elements, the error stack then pointed to offers not being defined in the show view.

Test that the 'save' function fails when invalid data is passed. (Ruby Rspec)

I am working on a project that has the following validation criteria in the "project" model:
validates :project_name,
presence: true,
format: { with: /\A[a-zA-Z\s_-]+\z/,
message: 'allows letters, spaces, underscores, and hyphens' }
validates :jira_id,
uniqueness: true,
presence: true,
format: { with: /\A[A-Z]+-[0-9]+\z/,
message: 'allows capitalized letters followed by a hyphen and numbers' }
validates :capacity,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :openings,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates_format_of :last_status_change_dt,
with: /\d{4}-\d{2}-\d{2}/,
message: 'must be formatted as YYYY-MM-DD',
on: :save
validates_presence_of :last_status_change_dt
The projects_controller:
def create
ActiveRecord::Base.transaction do
parameters = project_params.dup
parameters[:language_id] = get_language_id(params[:language_name]) if parameters[:language_id].nil?
#project = Project.new(parameters)
if #project.save
render json: #project, status: :created, location: #project
else
render json: #project.errors, status: :not_acceptable
raise ActiveRecord::Rollback
end
end
rescue ActiveRecord::ActiveRecordError
render json: { text: 'Project was not created' }, status: :internal_server_error
end
In the spec file, FactoryBot build and create functions are used.
In the tests right now, the validation is dependent on the project model, but I want to test the save function. How can I mock the save function to fail?
You can use allow_any_instance_of, but that's a bit of a hammer. Instead, make a double and mock Project.new to return it.
context 'Project#save raises ActiveRecord::ActiveRecordError' do
let(:project) {
# Set up the double to fail on save.
instance_double("Project").tap { |project|
allow(project).to receive(:save).and_raise(ActiveRecord::ActiveRecordError)
}
}
before {
# Set up Project.new to return the double
allow(Project).to receive(:new).and_return(project)
}
end
Note that it's quite unusual for save to raise ActiveRecord::ActiveRecordError, if it does something very unusual has gone wrong. You generally don't rescue it in a controller.
If your goal is to examine the response object and see if the json errors are returned correctly, I typically do something like this:
context "when saving the project fails" do
let(:params} do
{
language_id: 123,
}
end
let(:subject) { post :create, params: params }
before do
allow_any_instance_of(Project).to receive(:valid?) do |project|
project.errors.add(:language_id, 'fake validation reason')
false
end
end
it 'should return an error response' do
subject
expect(response).to have_http_status(422)
errors = response.parsed_body['errors']
expect(errors).to be_present
expect(errors['language_id']).to include 'fake validation reason'
end
end
You can also re-write the above code with instance_double.

Rails best_in_place Unprocessable Entity

I'm trying to edit the name field of a model with the best_in_place gem to edit items directly in line. When trying to do so I'm getting a an error 422 Unprocessable Entity.
I've done some research and found out in the response the controller is expecting not only the name attribute but also the group and some other attributes.
["Group must exist","Lumo can't be blank","Lumo is not a number"]
I have setup my controller in the correct way (I think).
materials_controller.rb
def update
#material = Material.find(params[:id])
if params[:commit] == "Save" then
success = #material.update(material_params)
params[:create_pure_composition] = false
else
#material = Material.new(material_params)
#material.user_id = current_user.id
success = #material.save
end
respond_to do |format|
if success
plot1 = prepare_e_plot(#material)
plot2 = prepare_st_plot(#material)
format.html { redirect_to #material }
format.json { render json: { plot1: plot1, plot2: plot2, status: 200 } }
else
format.html { render 'edit' }
format.json { respond_with_bip(#material) }
end
end
end
Is there a way with best_in_place to send these value's when updating the name attribute? I've tried with the params attribute from best_in_place.
<%= best_in_place #material, :name, as: :input, params: { group_id: #material.group_id } %>
This wasn't sending any extra params with the update.
Here's is what the Material model looks at.
material.rb
class Material < ActiveRecord::Base
belongs_to :user
belongs_to :group
validates :name, presence: true, uniqueness: {scope: :user_id}
validates :lumo, presence: true, numericality: true
end
Does anybody know why it's asking for other attributes and why best_in_place is not sending those along?
I figured out what the problem was and how to fix it. We use an option to Save or Save As when editing materials. In the controller we therefore check for the params[:commit].
By editing the url I was able to send in the params[:commit] with the update with best_in_place. Here is how the best_in_place code ended up like:
<%= best_in_place #material, :name, as: :input, url: material_path(#material, commit: "Save") %>

Rails:Receive POST with JSON array

I'm currently using a controller to receive POST with one json object at a time. And I want it change to receiving the whole array. How can I modify my controller?
Current Controller
def create
respond_to do |format|
#targetrecord = TargetRecord.new(targetrecord_params)
#targetrecord.save
if #targetrecord.save
format.json{ render :json => #targetrecord.to_json ,status: 200 }
else
format.json { render json: #targetrecord.errors, status: 404 }
end
end
end
end
def targetrecord_params
params.require(:targetrecord).permit(:id, :uuid, :manor, :mac, :beacon_type, :longitude, :latitude, :address, :findTime, :rssi, :finderID, :created_at, :updated_at )
end
I'm sending the POST as below right now
"targetrecord":
{"id":"","name":"",.....}
And I want to send multiple sets as an array like
"targetrecord":[
{"id":"1","name":"",.....},
{"id":"2","name":"",.....},
....]
How can I let my controller know that she needs to extract and create one by one? Thanks a lot!
If you are POSTing an array, then the array will just be part of your params object when processed by the controller action. So you should be able to loop through the array and create an array of TargetRecord objects. You'll need to modify your targetrecord_params method to allow it to accept an argument since you can't just look at 'params' in that context once you make the change. You'll also need to find a way to track whether or not all the records have saved successfully.
I haven't tested this code, but something like this should get you going in the right direction, I think:
def create
respond_to do |format|
#targetrecords = []
save_succeeded = true
params[:targetrecord].each do |record|
tr = TargetRecord.new(targetrecord_params(record))
save_succeeded = false unless tr.save
targetrecords << tr
end
if save_succeeded
format.json{ render :json => #targetrecord.to_json ,status: 200 }
else
format.json { render json: #targetrecord.errors, status: 404 }
end
end
end
end
def targetrecord_params(record)
record.require(:targetrecord).permit(:id, :uuid, :manor, :mac, :beacon_type, :longitude, :latitude, :address, :findTime, :rssi, :finderID, :created_at, :updated_at )
end

rails before_save callback does not trigger

In my rails app, I want to check an amount sent via a form before saving it to the DB. If the amount is too big, I want to set a boolean variable "confirmed" to false. Otherwise, its confirmed and true.
I entered this in my model:
# if amount is too big, set to unconfirmed
before_save do
if self.amount > 9999
self.confirmed = false
else
self.confirmed = true
end
end
Controller action (was scaffolded):
def create
#statement = Statement.new(statement_params)
respond_to do |format|
if #statement.save
format.html { redirect_to thankyou_path, notice: 'Successfully created.' }
format.json { render action: 'show', status: :created, location: #statement }
else
format.html { render action: 'new' }
format.json { render json: #statement.errors, status: :unprocessable_entity }
end
end
end
Testing this results in the following:
- if the amount is < 9999, the form gets saved, all good.
- if the amount is > 9999, the form does not get saved. It simply stays on the same page and nothing happens. No error message and nothing to see in the log except the fact that the data did not get entered into the database.
What do I do wrong?
It's because if amount is greater than 9999, the value returned from block is false (from self.confirmed = false line) - and if block (or method) passed into before_save returns false, ActiveRecord stops saving the record. So the simple solution is to add true that would be returned:
before_save do
if self.amount > 9999
self.confirmed = false
else
self.confirmed = true
end
true
end
The relevant piece of documentation for reference:
If a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last.
http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#module-ActiveRecord::Callbacks-label-Canceling+callbacks

Resources