Validate max amount af associated objects - ruby-on-rails

I have an Account model and a User model:
class Account < ActiveRecord::Base
has_many :users
end
class User < ActiveRecord::Base
belongs_to :account
end
Users belong to an account and an account have a user maximum (different for each account). But how do I validate that this maximum have not been reached when adding new users to an account?
First I tried to add a validation on the user:
class User < ActiveRecord::Base
belongs_to :account
validate :validate_max_users_have_not_been_reached
def validate_max_users_have_not_been_reached
return unless account_id_changed? # nothing to validate
errors.add_to_base("can not be added to this account since its user maximum have been reached") unless account.users.count < account.maximum_amount_of_users
end
end
But this only works if I'm adding one user at a time.
If I add multiple users via #account.update_attributes(:users_attributes => ...) it just goes directly through even if there is only room for one more user.
Update:
Just to clarify: The current validation method validates that account.users.count is less than account.maximum_amount_of_users. So say for instance that account.users.count is 9 and account.maximum_amount_of_users is 10, then the validation will pass because 9 < 10.
The problem is that the count returned from account.users.count will not increase until all the users have been written to the database. This means adding multiple users at the same time will pass validations since the user count will be the same until after they are all validated.
So as askegg points out, should I add validation to the Account model as well? And how should that be done?

If you call account.users.size instead of account.users.count it will also include users which have been built but not saved to the database.
HOWEVER this will not fully solve your problem. When you call account in a user it is not returning the same account instance that #account is pointing to so it does not know about the new users. I believe this will be "fixed" in Rails 3, but in the meantime I can think of a couple solutions.
If you are saving the account the same time you are adding users (which I assume so since you are calling update_attributes) then the validation can go in there.
# in account.rb
def validate_max_users_have_not_been_reached
errors.add_to_base("You cannot have more than #{maximum_amount_of_users} users on this account.") unless users.size < maximum_amount_of_users
end
I'm not sure how you are saving the associated models, but if account validation fails they should not be saved.
The other solution is to reset the user.account instance to self when updating user attributes. You could do this in the users_attributes setter method.
# in account.rb
def users_attributes=(attributes)
#...
user.account = self
#...
end
This way user's account will point to the same account instance so account.users.size should return the amount. In this case you would keep the validations in the user model.
It's a tricky problem but hopefully this gave you some ideas on how to solve it.

The reason it is passing is because update_attributes does not go through validations.
Also - your logic only checks for the existing number of account against their maximum permitted. There is no calculation considering the number of users attempting to be added. I would think this logic belongs more in the Account model (?).

Related

How do I prevent a user from being saved on a validation on an associated model in ruby on rails?

I have the following setup with the backend to a mobile app:
class User < ApplicationRecord
has_many :mobile_device_infos
...
end
class MobileDeviceInfo < ApplicationRecord
belongs_to :user
...
end
Every time the user logs in, a user device id is saved to mobile device info table, along with the last login time. A new device id will create a new record for the same user account. When a new user is created, then a search needs to be done via a validation that prevents the user from being saved if there are more than 2 duplicate accounts with the same device id where the last login time is less than one hour old. How can I create a validation that checks the mobile device info table each time a user is created that will prevent the user from being saved if there are more than 2 of the same device id?
I would imagine that I would need a validation on the MobileDeviceInfo table and accepts_nested_attibutes_for on the user model. How would I go about creating this type of validation?
possible when user login you should check if device already exist to prevent duplicates - MobileDeviceInfo.find_or_initialize_by(base_params). Or you can add custom validator validates_with MobileDeviceInfoCustomValidator to user model.
app/validators/mobile_device_info_custom_validator.rb
# frozen_string_literal: true
class MobileDeviceInfoCustomValidator < ActiveModel::Validator
def validate(record)
# your custom logic to validate
return if # valid_case_logic
error = I18n.t('validators.errors.messages') #your error if needed
record.errors.add(:mobile_device_info, error)
end
end
also all validation in MobileDeviceInfo model should work throw nested attributes.

Create User Account Settings Page in Ruby on Rails with devise

I am new to Ruby on Rails and I have created a project that contains a User table (generated by devise) and a AccountSetting table that contains user specific account settings (this table has a foreign key that relates to the id in the User table thus each User has zero or one AccountSettings). I have my seed data working fine, and I can seed the database with users that have user specific account settings. The User table is related to the AccountSetting table with a "has_one :accountsetting" and the AccountSettings table "belongs_to :user". This all works and makes sense. However, I have a method called "show_user_setting" in my UserSettings controller, and I do not know how to ONLY SHOW the account settings for that specific authenticated user.
So, how can I only display the user setting for the currently logged in user? Again, I am using devise.
My general idea of how to do this would be something like this. However I know this is incorrect, but for the purpose of an explanation, here it is.
def show_user_setting
#setting = AccountSetting.find(current_user)
end
My idea is that the #setting will contain the setting for the currently logged in user. Thanks in advance!
You should do this:
#app/models/account_setting.rb
class AccountSetting < ActiveRecord::Base
belongs_to :user
end
#app/models/user.rb
class User < ActiveRecord::Base
has_one :account_setting
end
This will allow you to call the following:
#setting = current_user.account_setting
Our Setup
For what it's worth, we do something similar:
#app/models/user.rb
class User < ActiveRecord::Base
before_create :build_profile #-> builds a blank profile on user create
has_one :profile
end
#app/models/profile.rb
class Profile < ActiveRecord::Base
belongs_to :user
end
This allows us to put all sorts of different options inside the profile model (we have homepage etc):
The important thing to note here is that the above allows you to delegate various methods to the profile model, allowing you to call the following:
current_user.profile_name
current_user.profile_signin_redirect?
current_user.profile_avatar
etc
Have you tried
def show_user_setting
#setting = AccountSetting.find_by(user_id: current_user.id)
end
The way .find() works is it searches the model for the id passed. So, the way you currently have it is your going to try to search for the id of the model, when you want to find the foreign key. So use Model.find_by(column_name: param). You'll what to change user_id: to the column name of what you're storing the foreign key in, I'm just assuming it's something similar to that.
I'm guessing the show_user_setting function is part of a controller, if it is on a model then read this: accessing devise current_user within model
to set the #setting variable you should be able to do this
#setting = AccountSetting.find(user_id: current_user.id)
or
#setting = AccountSetting.find(user: current_user)

What is the best way to validate a foreign key association in Rails belongs to current user

As an example, say I have a credit card that belongs to a user with an associated billing address.
One solution is that I assign the relationship in the create/update block. However, this really requires me to handle the errors such as blank id or non-existing record. Was hoping there was a better solution that i have overlooked.
I am interested in making sure that an address that belongs to another user is not assigned to the current user.
So, code for the create solution:
credit_card = current_user.credit_cards.create!(credit_card_params) do |credit_card|
credit_card.address = User.addresses.find(params['credit_card']['address_id'])
end
I haven't added the error handling yet and could push that to a class method in the User object and then call User.set_address('address_id') which also handles validation. Is there a better pattern for doing this?
If we look at it from the credit card point of view, you just want to make sure that the address_id is unique, meaning the address_id can't be used twice on two different credit cards.
class CreditCard < ActiveRecord::Base
validates :address_id, uniqueness: true
end
If a new credit card is initialized with an address_id that belongs to another user the credit card becomes invalid.
Update: by adding a scope to the uniqueness validation, you allow duplication for a smaller set, which in your case would be the user_id ( if 2 creditcards have the same address, and the same user then that's fine )
class CreditCard < ActiveRecord::Base
validates :address_id, uniqueness: { scope: user_id }
end

Rails default data

Every time I create a new company record in rails, I need to add some default (blank) contact records at that company. Front Desk, Receiving, HR, IT and so on...they won't have any data in them besides the name, just a placeholder for the user to fill in later.
So, my company model has_many contacts, and contacts belong_to company. The contact records are static and the same for every new company that gets added, but I need to pre-populate the contacts table with data, so my users don't have to.
I've read a lot about seeding the database, but I won't be able to use the terminal every time a user dynamically creates a company, and it needs to be dynamically tied to that company, the records are not agnostic. Seeding doesn't seem to be the right thing. How should this be done?
you should use a before_save filter, which checks if an attribute is empty, and otherwise set it to the default.
Using a before_save will guard against deletions later on.
But be careful only to do this for fields which will never be empty.
class Company < ActiveRecord::Base
has_many :contacts
before_save :add_defaults
def add_defaults
contacts ||= Contact.default_list # this only sets it if it's nil
# you can implement Contact#default_list as a method, or as a scope in the contacts model
end
end
What about after_create callback in Company Model?
Smth like this:
class Company < ActiveRecord::Base
has_many :contacts
after_create :add_contacts
def add_contacts
contacts.create(name: "Some name", phone: "...", ....)
end
end
Although it notionally exists for generating test data, the FactoryGirl gem is very useful for this purpose. Use it in conjunction with the after_save approach mentioned here, and you'll have a nice place to centrally define your blank records.

What is the best way of preventing the last record in a has_many collection being removed?

I have two ActiveRecord classes. A simplified view of these classes:
class Account < ActiveRecord::Base
has_many :user_account_roles
end
class UserAccountRole < ActiveRecord::Base
belongs_to :account
# Has a boolean attribute called 'administrator'.
end
What I'm struggling with is that I'd like to be able to apply two validation rules to this:
* Ensuring that the last UserAccountRole cannot be removed.
* Ensuring that the last UserAccountRole that is an administrator cannot be removed.
I'm really struggling to understand the best way of achieving this kind of structural validation. I've tried adding a before_remove callback to the association, but I don't like that this has to throw an error which would need to be caught by the controller. I'd rather this be treated as 'just another validation'.
class Account < ActiveRecord::Base
has_many :user_account_roles, :before_remove => check_remove_role_ok
def check_remove_relationship_ok(relationship)
if self.user_account_relationships.size == 1
errors[:base] << "Cannot remove the last user from this account."
raise RuntimeError, "Cannot remove the last user from this account."
end
end
end
I don't think this makes any difference, but I'm also using accepts_nested_attributes_for.
Why not use a simple validation on Account?
class Account < ActiveRecord::Base
has_many :user_account_roles
validate :at_least_one_user_account_role
validate :at_least_one_administrator_role
private
def at_least_one_user_account_role
if user_account_roles.size < 1
errors.add_to_base('At least one role must be assigned.')
end
end
def at_least_one_administrator_role
if user_account_roles.none?(&:administrator?)
errors.add_to_base('At least one administrator role must be assigned.')
end
end
end
This way nothing needs to be raised, and the record won't be saved unless there's at least one role, and at least one administrator role. Thus when you re-render your edit form on error, this message will show up.
You could place the validation on UserAccountRole. If it is the only UserAccountRole associated with the Account, then it can't be deleted.
An easier solution may be to question an underlying assumption of your design. Why have UserAccountRole be an AR backed model? Why not just make it a plain ruby class? Is the end user going to dynamically define roles? If not, then you could greatly simplify your dilemma by making it a regular ruby class.

Resources