Rails determine if objects from accepts_nested_attributes_for objects changed? - ruby-on-rails

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.

Related

Can I do Find_Or_Create in model before save?

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).

Using Rails model to update to change one boolean value based on another

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

Can I make Rails update_attributes with nested form find existing records and add to collections instead of creating new ones?

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

How Rails: If a project has tasks it should not be deleted: how can I fix this?

Hi I have a project and each project has tasks. A task belongs to a project. Before I delete a project I want to check if there are related tasks. If there are tasks I don't want to delete the project. If there are no associated tasks, the project should be deleted. Can you please help me with the code? What am I missing?
class Project < ActiveRecord::Base
before_destroy :check_tasks
def check_tasks
if Project.find(params[:id]).tasks
flash[:notice] = 'This project has tasks.'
redirect_to :action => 'list_projects'
end
end
end
Return false from the before_destroy method to prevent the instance from being destroyed.
The method should also return a meaningful error for troubleshooting.
class Project < ActiveRecord::Base
before_destroy :check_tasks
def check_tasks
if self.tasks.any?
errors.add_to_base "Project has tasks and cannot be destroyed."
return false
end
end
end
Note: flash[:notice] and params[:attr_name] can only be used from controllers.
You have a couple of problems here.
You don't (or shouldn't) have access to the params variable (it's available in controllers and views only, unless you're passing it to the model, which is probably not what you want).
Your if checks against project.tasks which is an array - even an empty array evaluates to true, so your other code branch will never occur no matter if the project has tasks or not.
You should probably be setting error messages for the view from your ProjectsController#destroy action, not in your model.
Solutions:
Change Project.find(params[:id]) to self - you want to check the tasks for every instance of the Project.
Change the check in your if statement from if self.tasks to if self.tasks.any? which returns the value you want (false if the array is empty, true otherwise).
Move the flash[:notice] from your model to your controller, as well as the redirect_to call, where they belong. This means your check_tasks method can be changed to the following:
code:
def check_tasks
return !self.tasks.any?
end
Should the check be self instead? (not sure where you getting the params[:id] from).
Haven't checked this out yet though - but since I need something similar for my Users model I'll see how that works out and get back to you.
class Project < ActiveRecord::Base
before_destroy :check_tasks
private
def check_tasks
#edited
if tasks.empty?
false
end
end

Rails 3 ActiveRecord validation based on user permissions

I'm shifting code from an application built in a non-standard custom PHP framework into Ruby on Rails (version 3). In the PHP version all the controllers are really fat, with thin models, which I've always disagreed with, so I'm enjoying the way Rails does validation at the model level, which is probably 90% of what's happening in these fat controllers currently.
One problem I'm facing, and unsure how to resolve however, is that of differing validation rules based on who's making the change to the model. For example, an administrator, or the original creator of the record should be able to do things like flag a record as deleted (soft delete) whereas everybody else should not.
class Something < ActiveRecord::Base
...
validates :deleted, :owned_by_active_user => true
...
end
class OwnedByActiveUserValidator < ActiveModel::EachValidator
validate_each(record, attr_name, attr_value)
# Bad idea to have the model know about things such as sessions?
unless active_user.admin? || active_user.own?(record)
record.errors.add :base, "You do not have permission to delete this record"
end
end
end
Since the model itself is (in theory) unaware of the user who is making the change, what's the "rails way" to do this sort of thing? Should I set the active user as a virtual attribute on the record (not actually saved to DB), or should I just perform these checks in the controller? I have to admit, it does feel strange to have the model checking permissions on the active user, and it adds complexity when it comes to testing the model.
One reason I'm keen to keep as much of this as possible in the model, is because I want to provide both an API (accessed over OAuth) and a web site, without duplicating too much code, such as these types of permissions checks.
It is really the controller's job to handle authorization, or to delegate authorization to an authorization layer. The models should not know about, nor have to care about, who is currently logged in and what his/her permissions are - that's the job of the controller, or whatever auth helper layer the controller delegates that to.
You should make :deleted in-attr_accessible to mass assignment via new, create, or update_attributes. The controller should check the authenticated user's authorizations separately and call deleted= separately, if the authenticated user is authorized.
There are several authorization libraries and frameworks to help with authorization or to function as an authorization layer, such as cancan.
I would solve this with a before_filter in my controller, instead of with validations in my model.
class SomethingController < ApplicationController
before_filter :require_delete_permission, :only => [:destroy]
def destroy
# delete the record
end
private
def require_delete_permission
unless current_user.is_admin || record.owner == current_user
flash[:error] = 'You do not have delete permissions'
redirect_to somewhere
end
end
end
I have come across the same issue in Rails 2.3 and finally come up with this solution. In your model you define some atribute, depending on which you switch on/off validation. Than you your control you set this attribute depending on the date available to controller (such as user privileges in your case) as follows:
Class Model < ActiveRecord::Base
attr_accessor :perform_validation_of_field1 #This is an attribute which controller will use to turn on/off some validation logic depending on the current user
validates_presence_of :field1, :if => :perform_validation_of_field1
#This validation (or any similar one) will occur only if controller sets model.perform_validation_of_field1 to true.
end
Class MyController < ActionController::Base
def update
#item = Model.find(params[:id])
#item.update_attribute(params[:item])
#The controller decides whether to turn on optional validations depending on current user privileges (without the knowledge of internal implementation of this validation logic)
#item.perform_validation_of_field1 = true unless active_user.admin?
if #item.save
flash[:success] = 'The record has been saved'
redirect_to ...
else
flash.now[:error] = 'The record has not passed validation checks'
render :action => :edit
end
end
I think that in Rails 3 it can be done in similar manner.

Resources