Rails: corret way of validate number of records - ruby-on-rails

In my case, user has many categories, and I want to prevent user to delete his last category, this is what I did in Categories#destroy:
def destroy
if current_user.categories.count > 1
#category.destroy
redirect_to categories_url
else
flash[:category_notice] = "Cannot destroy the last category"
redirect_to categories_url
end
end
Obviously, this solves the problem. But I want to know if there is a corret way of doing it? Or this is just fine?

I would prefer to do it in the model (thatway you can prevent destroying the last record when you are working on the console too).
For ex you can use callback inside your Category-model:
before_destroy: check_if_last
def check_if_last
raise "Can't destroy last category!" if self.user.categories.count == 1
end
EDIT: To avoid race condition on user object, locking should do.

I would add a counter onto the user. So a user has a categories_count field, with a default of 0.
add_column :users, :categories_count, :integer, default: 0
then
class User
has_many :categories
end
class Category
belongs_to :user, counter_cache: true
end
You might want to use reset_counters:
User.find_each do |u|
User.reset_counters(u.id, :categories)
end
To set the counters to the appropriate values.
Then you can do
if current_user.categories_count > 1
#category.destroy
redirect_to categories_url
else
flash[:category_notice] = "Cannot destroy the last category"
redirect_to categories_url
end
This way the count is cached and it saves you a query. It's not much of a difference and your way is fine.

Related

check if parent record has any child records with start_date in the future then stop update rails

I have table called items and i have a method that called archive in the items_controller.rb, if the method gets called it sets the active field on the item to false. the issue is i have another table called reservations and a field on it called start_date so i want before it update the active field on item to false, i want to check if their is any reservations with start_date field in the future. if there is then do not update the active field on item and give a user an error message.
here is item.rb
class Item < ApplicationRecord
has_many :reservations
before_save :check_for_reservations
def check_for_reservations
if reservations.count > 0
reservations.each do |reservation|
if reservation.start_date > Date.today
return false
end
end
end
end
end
here is items.controller.rb
def archive
#item = Item.find(params[:id])
#item.active = false
if #item.save
flash[:notice] = "Item been archived..."
else
flash[:alert] = "Something went wrong..."
end
redirect_back(fallback_location: request.referer)
end
is not working its updating the item record the active equal = false
I think it would be reasonable to use a custom validation method which you can read about in the guide. It might look something like this:
class Item < ApplicationRecord
has_many :reservations
before_save :check_for_reservations
validate :okay_to_archive
def okay_to_archive
if !item.active && reservations.where("start_date >= ?", Date.today).any?
errors.add(:active, "can't be set to false when there are future reservations")
end
end
end
!item.active checks to see if the active attribute is not true (either nil or false). And reservations.where("start_date >= ?", Date.today).any? checks to see if there are any associated reservations that have a start_date that comes after today. So, together:
if !item.active && reservations.where("start_date >= ?", Date.today).any?
...
end
they check to see if the item is inactive with future reservations. If so, then an error is written to the errors:
errors.add(:active, "can't be set to false when there are future reservations")
and when you try to do #item.save, you'll get a validation error.
def archive
#item = Item.find(params[:id])
#item.active = false
if #item.save
flash[:notice] = "Item been archived..."
else
flash[:alert] = "Something went wrong..."
end
redirect_back(fallback_location: request.referer)
end
Code is not tested, but should be close.

Rails: first_or_create not saving

My goal for my application is to only show a form page with existing data or a blank form if new. I've accomplished this by using a callback that created a blank record when the user is created.
User model:
before_create :build_health_profile
However, if for whatever reason a users "health_profile" were to be destroyed or non-existant, it breaks my entire app with:
"undefined method `health_profile' for nil:NilClass"
It was mentioned to me that the "first_or_create" method could solve this by show a new form or finding the existing one, but I can't get it to save the fields. It directs to my root with my save alert like it saved, but nothing gets actually saved.
Controller:
class HealthProfilesController < ApplicationController
def new
#health_profile = current_user.build_health_profile
end
def create
#health_profile = HealthProfile.where(user_id: current_user).first_or_create(health_profile_params)
if #health_profile.save
flash[:success] = "Health profile saved."
redirect_to root_path
else
render 'new'
end
end
private
def health_profile_params
params.require(:health_profile).permit(
:age,
:weight,
:height,
:gender
)
end
end
I've seen where I could use a block for "first_or_create", but no luck getting that to work.
View:
<%= link_to "Health Profile", new_health_profile_path %>
Models:
class User < ActiveRecord::Base
has_one :health_profile, dependent: :destroy
end
class HealthProfile < ActiveRecord::Base
belongs_to :user
end
If you use first_or_create then that calls the save method as part of it on the record and tries to save that in the database. If it can't save the record, then the transaction is rolled back. So, you want to use: first_or_initialize here which is like new and does not save the record in the database immediately. It just loads the data. So, you can call save on it in the next line of your code.
So, in your code, where you have:
#health_profile = HealthProfile.where(user_id: current_user).first_or_create(health_profile_params)
Here you are not controlling the save part, that's already being done by the first_or_create method.
So, you actually want to just load the object (NOT save yet) by using first_or_initialize:
#health_profile = HealthProfile.where(user_id: current_user).first_or_initialize(health_profile_params)
and then, in the next line, you can call the save and based on it's return value you can take the decision:
if #health_profile.save
# do stuff if successfully saved health_profile
else
# otherwise
render 'new'
end
Because you have #health_profile.save,
You should change first_or_create into first_or_initialize
first_or_create immediately trigger save, whereas first_or_initialize would just assign the values to a New record or to an already existing record if record exists already
I was able to fix the problem of the record resetting itself when going back to the form by adjusting the new action. Thats everyone for the help.
def new
#health_profile = current_user.health_profile || HealthProfile.new
end
def create
#health_profile = HealthProfile.where(user_id: current_user).first_or_initialize(health_profile_params)
if #health_profile.save
flash[:success] = "Health profile saved."
redirect_to root_path
else
render 'new'
end
end

Dealing with a unique error

So I have got a database called Awards.
Users are able to 'award' a recipe but they can only do this once. The award database consists of recipe_id and user_id. I have made both of these columns unique so it wont allow you to award the recipe more than once. This works fine and if you attempt to award the recipe a second time I get this error:
columns user_id, recipe_id are not unique
Is there some code I can add into th create action to check for this error and then render a flash error message such as "already awarded recipe" instead of showing the error console?
this is my create method:
def create
#award = current_user.awards.build(award_params)
if #award.save
flash[:success] = "Award Given!"
redirect_to recipe_path(params[:recipe_id])
else
render 'static_pages/home'
end
end
Thanks,
Mike
There is a whole section of rails called validations that you're hitting on. The documentation is here: link. To get you basically set up, you could:
# award.rb
class Award < ActiveRecord::Base
validates_uniqueness_of :user_id, :recipe_id
end
# awards_controller.rb
def create
#award = current_user.awards.build(award_params)
if #award.save
flash[:success] = 'Award Given!'
redirect_to recipe_path(params[:recipe_id])
else
flash[:error] = 'There was an error awarding this award!'
render 'static_pages/home'
end
end

I want to destroy data from relationships but I can't

I want user to remove group that user attending groups by removing groupings.
So, I tried write below the code , but when run leave action , it happen error that
Unknown key: group_id.But I don't know how to deal with it . Please some help.
Thanks in advance.
GroupingsController.rb
def leave
#user = current_user
#group = Group.find(params[:id])
#user.remove(#group)
redirect_to :back , notice: "Destroy!"
end
User.rb
has_many :groups, :through => :groupings,:source => :group
def remove(group)
groupings.find_by_group_id(:group_id => group).destroy
end
# attend method is work correctly.
def attend(group)
groupings.create(:group_id => group)
end
You've already specified that you're using group_id (by saying find_by_group_id) - so you don't need to specify it again by passing it as a 'key' (eg :group_id =>)
So your code should just be
def remove(group)
groupings.find_by_group_id(group.id).destroy
end

Can I access information from one associated AR object in another when both are unsaved?

Say I open a Rails (2.3.8) script console and try this:
a = Account.new(:first_name) = 'foo'
i = a.invoices.build
p i.account.first_name
Account.rb is a model object and contains:
has_many :invoices
and Invoice.rb is a model as well containing:
belongs_to :account, :validate => true
In console line 3 above, i.account is nil. I realize that i.account would not be nil if account had been saved, but I do not wish to save an account unless I can create a valid invoice for the account. And, just for kicks, the invoice validation depends on some properties of the unsaved account.
Any ideas how to make this work?
Best,
Will
I typically do this with transactions. With rails transactions you can perform db interactions and roll them back at any time if something fails to validate. For example:
in your model:
def save_and_create_invoice
Account.transaction do
#first let's save the account, this will give us an account_id to work with
return false unless self.save
invoice = self.invoices.build
#setup your invoice here and then save it
if invoice.save
#nothing wrong? return true so we know it was ok
return true
else
#add the errors so we know what happened
invoice.errors.full_messages.each{|err| errors.add_to_base(err)}
#rollback the db transaction so the account isn't saved
raise ActiveRecord::Rollback
#return false so we know it failed
return false
end
end
end
And in your controller you would call it like so:
def create
#account = Account.new(params[:account])
respond_to do |format|
if #account.save_and_create_invoice
format.html
else
format.html {render :action => "new"}
end
end
end
Note that I didn't run this code to test it, just whipped it out real quick to show an example.

Resources