So i'm looking to move a find_or_create function from my controller to my model. Bascially, if the location already exists then choose that, if not then create a new one. From a bit of reading, I think a before save function should do it, but I'm not sure on the correct syntax and can't seem to find many examples anywhere.
Location.rb
before_save :get_locations
def get_locations
Location.find_or_create_by(name: [:name])
end
Here's my controller; it was working fine when running the find_or_create here.
Locations_controller.rb
def create
#location = Location.new(location_params)
# == worked previously == #
# #location = Location.find_or_create_by(name: location_params[:name])
# == worked previously == #
respond_to do |format|
...
end
end
Help would be great!
First I'll try to guess, what is that you want to do there. Considering what you've given us, you are trying to prevent the creation of several locations with the same name and if a user tries to do that, find him the already created location instead of creating a new one.
If that's true, then there're couple things to mention:
You don't need any before_create methods there. All the model needs to have is a validates_uniqueness_of :name call so there would never be 2 locations with the same name.
You need to move that creation logic back into the controller. You can use the find_or_initialize_by(name:location_params[:name]) call (in case you want to do something with the found record afterwards) or find_or_create_by(name:location_params[:name]) (to create it right away).
Related
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 currently trying to implement simple audit for users (just for destroy method). This way I know if the user has been deleted by an admin or user deleted itself. I wanted to add deleted_by_id column to my model.
I was thinking to use before_destroy, and to retrieve the user info like described in this post :
http://www.zorched.net/2007/05/29/making-session-data-available-to-models-in-ruby-on-rails/
module UserInfo
def current_user
Thread.current[:user]
end
def self.current_user=(user)
Thread.current[:user] = user
end
end
But this article is from 2007, I'm not sure will this work in multithreaded and is there something more up to date on this topic, has anyone done something like this lately to pass on the experience?
Using that technique would certainly work, but will violate the principle that wants the Model unaware of the controller state.
If you need to know who is responsible for a deletion, the correct approach is to pass such information as parameter.
Instead of using callbacks and threads (both represents unnecessary complexity in this case) simply define a new method in your model
class User
def delete_user(actor)
self.deleted_by_id = actor.id
# do what you need to do with the record
# such as .destroy or whatever
end
end
Then in your controller simply call
#user.delete_user(current_user)
This approach:
respects the MVC pattern
can be easily tested in isolation with minimal dependencies (it's a model method)
expose a custom API instead of coupling your app to ActiveRecord API
You can use paranoia gem to make soft deletes. And then I suggest destroying users through some kind of service. Check, really basic example below:
class UserDestroyService
def initialize(user, destroyer)
#user = user
#destroyer = destroyer
end
def perform
#user.deleted_by_id = #destroyer.id
#user.destroy
end
end
UserDestroyService.new(user, current_user).perform
Scenario: I have a has_many association (Post has many Authors), and I have a nested Post form to accept attributes for Authors.
What I found is that when I call post.update_attributes(params[:post]) where params[:post] is a hash with post and all author attributes to add, there doesn't seem to be a way to ask Rails to only create Authors if certain criteria is met, e.g. the username for the Author already exists. What Rails would do is just failing and rollback update_attributes routine if username has uniqueness validation in the model. If not, then Rails would add a new record Author if one that does not have an id is in the hash.
Now my code for the update action in the Post controller becomes this:
def update
#post = Post.find(params[:id])
# custom code to work around by inspecting the author attributes
# and pre-inserting the association of existing authors into the testrun's author
# collection
params[:post][:authors_attributes].values.each do |author_attribute|
if author_attribute[:id].nil? and author_attribute[:username].present?
existing_author = Author.find_by_username(author_attribute[:username])
if existing_author.present?
author_attribute[:id] = existing_author.id
#testrun.authors << existing_author
end
end
end
if #post.update_attributes(params[:post])
flash[:success] = 'great!'
else
flash[:error] = 'Urgg!'
end
redirect_to ...
end
Are there better ways to handle this that I missed?
EDIT: Thanks for #Robd'Apice who lead me to look into overriding the default authors_attributes= function that accepts_nested_attributes_for inserts into the model on my behalf, I was able to come up with something that is better:
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
if author_attributes[:id].nil? and author_attributes[:username].present?
author = Radar.find_by_username(radar_attributes[:username])
if author.present?
author_attributes[:id] = author.id
self.authors << author
end
end
end
assign_nested_attributes_for_collection_association(:authors, authors_attributes, mass_assignment_options)
end
But I'm not completely satisfied with it, for one, I'm still mucking the attribute hashes from the caller directly which requires understanding of how the logic works for these hashes (:id set or not set, for instance), and two, I'm calling a function that is not trivial to fit here. It would be nice if there are ways to tell 'accepts_nested_attributes_for' to only create new record when certain condition is not met. The one-to-one association has a :update_only flag that does something similar but this is lacking for one-to-many relationship.
Are there better solutions out there?
This kind of logic probably belongs in your model, not your controller. I'd consider re-writing the author_attributes= method that is created by default for your association.
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
author_to_update = Author.find_by_id(author_attributes[:id]) || Author.find_by_username(author_attributes[:username]) || self.authors.build
author_to_update.update_attributes(author_attributes)
end
end
I haven't tested that code, but I think that should work.
EDIT: To retain the other functionality of accepts_nested_Attributes_for, you could use super:
def authors_attributes=(authors_attributes)
authors_attributes.each do |key, author_attributes|
authors_attributes[key][:id] = Author.find_by_username(author_attributes[:username]).id if author_attributes[:username] && !author_attributes[:username].present?
end
super(authors_attributes)
end
If that implementation with super doesn't work, you probably have two options: continue with the 'processing' of the attributes hash in the controller (but turn it into a private method of your controller to clean it up a bit), or continue with my first solution by adding in the functionality you've lost from :destroy => true and reject_if with your own code (which wouldn't be too hard to do). I'd probably go with the first option.
I'd suggest using a form object instead of trying to get accepts_nested_attributes to work. I find that form object are often much cleaner and much more flexible. Check out this railscast
I am aware of the basic dirty indicator methods for rails, which work if direct attributes of an object have changed, I'm wondering how to determine if my children were updated..
I have a form for a collection of files, we'll call it a folder. A folder accepts_nested_attributes_for :files. What I need to determine (within the controller action) is whether or not the files that are within the params hash are different from the ones that are in the db.. So, did the user delete one of the files, did they add a new file, or both (delete one file, and add another)
I need to determine this because I need to redirect the user to a different action if they deleted a file, versus adding a new file, versus just updated attributes of the folder.
def update
#folder = Folder.find(params[:id])
#folder.attributes = params[:folder]
add_new_file = false
delete_file = false
#folder.files.each do |file|
add_new_file = true if file.new_record?
delete_file = true if file.marked_for_destruction?
end
both = add_new_file && delete_file
if both
redirect_to "both_action"
elsif add_new_file
redirect_to "add_new_file_action"
elsif delete_file
redirect_to "delete_file_action"
else
redirect_to "folder_not_changed_action"
end
end
Sometimes you want to know that folder is changed without determining how. In that case you can use autosave mode in your association:
class Folder < ActiveRecord::Base
has_many :files, :autosave => true
accepts_nested_attributes_for :files
attr_accessible :files_attributes
end
Then in controller you can use #folder.changed_for_autosave? which returns whether or not this record has been changed in any way (new_record?, marked_for_destruction?, changed?), including whether any of its nested autosave associations are likewise changed.
Updated.
You can move model specific logic from controller to a method in folder model, e.q. #folder.how_changed?, which can return one of :add_new_file, :delete_file and etc. symbols (I agree with you that it's a better practice, I'd just tried to keep things simple). Then in controller you can keep logic pretty simple.
case #folder.how_changed?
when :both
redirect_to "both_action"
when :add_new_file
redirect_to "add_new_file_action"
when :delete_file
redirect_to "delete_file_action"
else
redirect_to "folder_not_changed_action"
end
This solution uses 2 methods: new_record? and marked_for_destruction? on each child model, because Rails in-box method changed_for_autosave? can tell only that children were changed without how. This is just the way how to use this indicators to achieve your goal.
I have a permission model in my app, that ties (Users, Roles, Projects) together.
What I'm looking to learn how to do is prevent a user for removing himself for their project...
Can you give me feedback on the following?
class Permission < ActiveRecord::Base
.
.
.
#admin_lock makes sure the user who created the project, is always the admin
before_save :admin_lock
def before_save
#Get the Project Object
project = Find(self.project_id)
if project.creator_id == current_user.id
# SOME HOW ABORT OR SEND BACK Not Allowed?
else
#continue, do nothing
end
end
end
Is that look like the right approach?
Also, I'm not sure how to do the following two things above:
How to abort prevent the save, and send back an error msg?
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Thanks for reading through
How to abort prevent the save, and send back an error msg?
return false during the callback chain tells activemodel to stop (similar to how adding errors to the model during a validation tells it to stop at that point)
self.errors.add_to_base "msg" will add an error to the model, which can then be rendered on the view.
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Models shouldn't really know about things like the current request, if at all possible, you should be locking things down at the controller/action level.
EDIT:
So, the role of controllers is to deal with everything involved in getting the correct information together based on the request, and passing it to the view (which becomes the response). People often say "make your models fat and your controllers skinny", but that could be said of any system that embraces object oriented design -- your logic should be in objects when possible.
That being said, the whole point of controllers is to deal with routing the right things to the right places, and authentication is definitely a concern of routing.
You could easily move the line comparing creator_id to user id in the action, and react based on that.
Now, sometimes you genuinely need that stuff in the model and there is no way around it. That becomes a problem, because you need to fight rails to get it there. One way would be to attr_accessor a current_user field on your model, and pass that in on initialize. Another would be to remove the fields from the params hash that a user is not allowed to change in the action. Neither is really that nice though.
Agreed with Matt that you should try to use the controller for the redirect. The model should have the logic to determine if the redirect is appropriate. Maybe something like
class ProjectsController < ApplicationController
def update
redirect_to(projects_url, :alert => "You can't remove yourself from this project.") and return if Role.unauthorized_action?(:update, params[:project])
#project = Project.find(params[:id])
if #project.update_attributes(params[:project])
...
end
class Role
def self.unauthorized_action?(action, params)
# your logic here
end
You should check out CanCan for some ideas.
In permission model take one field project_creater as boolean
In project modelbefore_create :set_project_ownership
def set_project_ownership
self.permissions.build(user_id: User.current.id, project_creater: true)
end
In project controllerbefore_filter :set_current_user
In Application controllerdef set_current_user
User.current = current_user
end