I'm looking to pass arguments from the controller to the model, but I keep getting wrong number of arguments.
Model:
before_create :embed_info
def embed_info(embed_url)
something = embed_url
## Do some stuff
self.title = "Title" ##This works.
end
Controller:
Create action:
#post = Post.new post_params
#post.embed_info(params[:title])
if #post.save
redirect_to root_url, notice: 'post created'
else
render 'new'
end
You can not pass arguments from controller to model callback like this.
You can use attr_accessor to create a virtual attribute and then set that attribute as part of your create action from controller.
If you're manually invoking the embed_info method, you shouldn't also be invoking it automatically via a before_create callback. Remove the line before_create :embed_info as it's currently serving no purpose except to invoking embed_info with no arguments when you attempt to save your model.
It's possible that you intended to add a validation, which is different than a before_create callback. If your intent was to make sure that embed_info had been called then you should use validations to insure that whatever side effects embed_info has leave the model in a valid state.
but I keep getting wrong number of arguments.
You are getting an arguments error because of this: before_create :embed_info. You can delete that line and then explicitly call the method like you are already doing in the controller.
This is probably not best practice but I think it will get your code working.
Another thing you could do is to move the logic for deriving the title to the controller and then pass the title in with the params.
# controller
def create
#post = Post.new post_params.merge(title: embed_info)
...
def embed_info
something = params[:title]
## Do some stuff
...
You could use attr_accessor to create a virtual attribute of embed_url. Pass it in new action itself. And when you call save, before_save will be called by itself and you can use embed_url there (as its a virtual attribute).
Let me know if you need help if the explanation is not clear.
Related
Say I have two models, Email and Message, with a boolean read attribute, and to mark them read I add a concern with mark_read and mark_unread patch members that route to ReadablesController.
I'd like to make it so that set_readable is automatic, not requiring me to manually query the params, and instead just work for all models with a read attribute. Is there a simple way to accomplish that?
class ReadablesController < ApplicationController
before_action :set_readable
...
def mark_read
#readable.read = true
#readable.save
flash[:notice] = "#{#readable.class.to_s} marked read."
redirect_to :back
end
def mark_unread
#readable.read = false
#readable.save
flash[:notice] = "#{#readable.class.to_s} marked unread."
redirect_to :back
end
private
def set_readable
throw "error" if params[:email_id].present? && params[:message_id].present?
#readable = Email.find(params[:email_id]) if params[:email_id].present?
#readable = Message.find(params[:message_id]) if params[:message_id].present?
end
end
You can check if a model has read attribute with has_attribute?(:read). From there it is trivial to call your mark_read and mark_unread methods.
#model.mark_read if #model.has_attribute?(:read)
This probably goes to your controller's set_readable method where it still will have to check a relevant param, say, params[:read] to invoke the logic.
I've a method named update inside my DailyOrdersController:
def update
if #daily_order.update( daily_order_params.merge({default_order:false}) )
respond_or_redirect(#daily_order)
else
render :edit
end
end
My DailyOrder model:
before_save :refresh_total
def refresh_total
# i do something here
end
What I'm trying to do now is, I want the refresh_total callback to be skipped if the update request is coming from current_admin.
I have 2 user model generated using Devise gem:
User (has current_user)
Admin (has current_admin)
I try to make it like this:
def update
if current_admin
DailyOrder.skip_callback :update, :before, :refresh_total
end
if #daily_order.update( daily_order_params.merge({default_order:false}) )
respond_or_redirect(#daily_order)
else
render :edit
end
end
But it's not working and still keep calling the refresh_total callback if the update request is coming from current_admin (when the logged-in user is admin user).
What should I do now?
I think this is all what you need:
http://guides.rubyonrails.org/active_record_callbacks.html#conditional-callbacks
If you skip callback, you should enable it later. Anyway, this does not look as the best solution. Perhaps you could avoid the callbacks otherwise.
One way would be to use update_all:
DailyOrder.where(id: #daily_order.id).update_all( daily_order_params.merge({default_order:false}) )
Or you could do something like this:
#in the model:
before_validation :refresh_total
#in the controller
#daily_order.assign_attributes( daily_order_params.merge({default_order:false}) )
#daily_order.save(validate: current_admin.nil?)
or maybe it would be the best to add a new column to the model: refresh_needed and then you would conditionally update that column on before_validation, and on before_save you would still call the same callback, but conditionally to the state of refresh_needed. In this callback you should reset that column. Please let me know if you would like me to illustrate this with some code.
This may come in handy:
http://www.davidverhasselt.com/set-attributes-in-activerecord/
UPDATE
Even better, you can call update_columns.
Here is what it says in the documentation of the method:
Updates the attributes directly in the database issuing an UPDATE SQL
statement and sets them in the receiver:
user.update_columns(last_request_at: Time.current)
This is the fastest way to update attributes because it goes straight to
the database, but take into account that in consequence the regular update
procedures are totally bypassed. In particular:
\Validations are skipped.
\Callbacks are skipped.
+updated_at+/+updated_on+ are not updated.
This method raises an ActiveRecord::ActiveRecordError when called on new
objects, or when at least one of the attributes is marked as readonly.
I'm trying to make a pet rails app. My pet model includes two boolean values, hungry and feed_me. Right now, hungry and feed_me can both be set in the view, but I'm trying to set up the model so that if feed_me is true, hungry will automatically be changed to false. No matter what I do, however, feed_me never resets hungry. This is what I have in the model now:
attr_accessor :feed_me
before_save :feed
def feed
#feed_me = Creature.find(params[:feed_me])
#hungry=Creature.find(params[:hungry])
if #feed_me==true
#hungry=false
end
end
I'm new to Rails, but my understanding is that model should have access to the params hash, so I'm confused about why I can't use it to reset values.
You're on the right track using model callbacks, however models don't have access to the param hash - its available to controllers.
The model already knows the value of it's own attributes, so you don't need to get them from params. The controller I imagine is updating feed_me.
Also you shouldn't need to declare feed_me as an attr_accessor assuming it is backed by a database column.
You can change before_save to:
def feed
if self.feed_me
self.hungry = false
end
end
In your controller, I imagine you'd do something like:
def update
pet = Pet.find(params[:id])
pet.feed_me = params[:feed_me]
if pet.save
redirect_to pet_path(pet)
else
flash[:notice] = 'Error saving pet'
render :edit
end
end
Let's say you have simple model called Project and you need to store it's short url for later usage, when is the best way to compute it?
The best solution I have for now is a after_create hook, but that leads to code like
short_url || Rails.application.routes.url_helpers.project_path(project, host: HOSTNAME)
It does not feel right to access the url from the model.
In short, where do you put the code to compute the short_url?
Thank you,
I would add this code in the controller.
class ProjectsController < ApplicationController
def create
#project = Project.new(params[:project])
respond_to do |format|
if #project.save
#project.update_attribute(:short_url, project_url(#project))
(..)
end
end
It feels like this code belongs to the controller as it deals with http/url. Storing in the db sounds ok, but asking for the url is the controller's responsibility.
The line:
#project.update_attribute(:short_url, project_url(#project))
needs to be added below the call to .save (or .create), as only then the project_url helper can be called (the project object got its id already).
Is there a reason why you dont want to save the exact short_url instead? So when you need the url for the project object, you can just check if the short_url is present or not. I believe you can just add a decorator to determine the url of project.
#using Draper
class ProjectDecorator < Draper::Decorator
def effective_url
source.short_url.present? ? source.short_url : project_path(source)
# or source.short_url || project_path(source) if short_url will not be an empty string
# or source.short_url || source
end
end
Is it possible to send variables in the the transition? i.e.
#car.crash!(:crashed_by => current_user)
I have callbacks in my model but I need to send them the user who instigated the transition
after_crash do |car, transition|
# Log the car crashers name
end
I can't access current_user because I'm in the Model and not the Controller/View.
And before you say it... I know I know.
Don't try to access session variables in the model
I get it.
However, whenever you wish to create a callback that logs or audits something then it's quite likely you're going to want to know who caused it? Ordinarily I'd have something in my controller that did something like...
#foo.some_method(current_user)
and my Foo model would be expecting some user to instigate some_method but how do I do this with a transition with the StateMachine gem?
If you are referring to the state_machine gem - https://github.com/pluginaweek/state_machine - then it supports arguments to events
after_crash do |car, transition|
Log.crash(car: car, crashed_by: transition.args.first)
end
I was having trouble with all of the other answers, and then I found that you can simply override the event in the class.
class Car
state_machine do
...
event :crash do
transition any => :crashed
end
end
def crash(current_driver)
logger.debug(current_driver)
super
end
end
Just make sure to call "super" in your custom method
I don't think you can pass params to events with that gem, so maybe you could try storing the current_user on #car (temporarily) so that your audit callback can access it.
In controller
#car.driver = current_user
In callback
after_crash do |car, transition|
create_audit_log car.driver, transition
end
Or something along those lines.. :)
I used transactions, instead of updating the object and changing the state in one call. For example, in update action,
ActiveRecord::Base.transaction do
if #car.update_attribute!(:crashed_by => current_user)
if #car.crash!()
format.html { redirect_to #car }
else
raise ActiveRecord::Rollback
else
raise ActiveRecord::Rollback
end
end
Another common pattern (see the state_machine docs) that saves you from having to pass variables between the controller and model is to dynamically define a state-checking method within the callback method. This wouldn't be very elegant in the example given above, but might be preferable in cases where the model needs to handle the same variable(s) in different states. For example, if you have 'crashed', 'stolen', and 'borrowed' states in your Car model, all of which can be associated with a responsible Person, you could have:
state :crashed, :stolen, :borrowed do
def blameable?
true
end
state all - [:crashed, :stolen, :borrowed] do
def blameable?
false
end
Then in the controller, you can do something like:
car.blame_person(person) if car.blameable?