I have two objects, Project and User, which are joined by an object called ProjectAssignment. The ProjectAssignments object has an additional field: project_role. Models shown below.
class Project < ActiveRecord::Base
#relationships
has_many :project_assignments
has_many :users, :through => :project_assignments
end
class ProjectAssignment < ActiveRecord::Base
belongs_to :project
belongs_to :user
belongs_to :project_role
end
class User
has_many :project_assignments
has_many :projects, :through => :project_assignments
end
I have to validate that for a given project, there is exactly one ProjectAssignment with a project_role of "Principal Investigator" at any time. I'm a little unsure how to write a validation in the ProjectAssignment model. If I unset the current PI first then there is less than 1 Principal Investigator and if set a user to Principal Investigator before unsetting the other there is more than 1.
class ProjectAssignment
validates :allow_exactly_one_pi
def require_exactly_one_pi
if self.project_role.name == 'Principal Investigator' and other_princ_inv_exists
#more than one principle investigator set => error
elseif was_principle_investigator
#no principle investigator set => error
end
end
end
Any suggestions how this should be handled?
Very nice problem to chew on.
First you have to expose something about one model to another, there is a specific kind of role (principal investigator), and ProjectAssignment must be aware of that 'special case' role. But! it should be the project role that keeps track of that special status, so I'd add a method to the ProjectRole model:
ProjectRole < ActiveRecord::Base
def ispi?
self.name == 'Principal Investigator'
end
end
Then you have to figure out how to iterate over all project_asssignments and determine if any of them are a principal investigator. You have to access a Class method from an instance of a project_assignment.
class ProjectAssignment < ActiveRecord::Base
validate :there_can_only_be_one_principal_investigator
def there_can_only_be_one_principal_investigator
error = false
self.class.where('project_id = ?',self.project_id).each do |p|
if p.project_role.ispi?
error = true
break
end
end
if error
#whatever
end
end
end
Now you have to change your associations, ProjectAssignment can only have ONE role, so
class ProjectAssignment < ActiveRecord::Base
belongs_to :project
belongs_to :user
has_one :project_role
end
So now you are assured you cannot add a ProjectAssignment with project_role == Principal Investigator, if a Principal Investigator already exists for that Project.
What about an update, you update a ProjectAssignment with the project_role == PI, and there already is another ProjectAssignment with a PI, the validation will catch that.
Now how do you assure there is at least ONE PI? I think what that means is the FIRST ProjectAssignment for any one Project MUST be a PI. This is where you have to be a little hackish, you have to expose knowledge about the ProjectRole model directly in the ProjectAssignment model.
validate :there_must_be_at_least_one_principal_investigator
def there_must_be_at_least_one_principal_investigator
if self.class.where('project_id = ?', self.project_id).count() == 0 AND !self.project_role_id == 1
#error
end
end
I don't really like this solution, because the fact that the PI roles's id is 1 (or whatever it is), is hardcoded in another Model!! To make it a little LESS objectionable, you could add a Class method to the ProjectRole model
class ProjectRole < ActiveRecord::Base
def self.piid
1 # or whatever it is
end
end
Then do this:
def there_must_be_at_least_one_principal_investigator
if self.class.where('project_id = ?', self.project_id).count() == 0 AND ! self.project_role_id == ProjectRole.piid
#error
end
end
Now, how do you change the principal investigator? You'll have to do that in a seperate action, i.e.
class ProjectAssignmentController < ApplicationController
def change_pi
#proj_assignment1 = ProjectAssignment.find(params[:orig_pi_id])
#proj_assignment2 = ProjectAssignment.find(params[:new_pi_id])
#proj_assignment1.project_role_id = params[:new_role_for_orig_pi].to_i
#proj_assignment1.save :validate=>false # it's OK, you're taking care of it below
#proj_assignment2.project_role_id = ProjectRole.piid
#proj_assignment2.save
end
end
Here is a completely different approach, in summary format:
1) When you Create a new Project, ask for the User who should be the PI, and build the first ProjectAssignment record, that eliminates the need to validate that there is at least one PI.
2) In ProjectController and in your projects edit view, create a 'change PI' interface of some kind, in that interface you'd need to know the user_id of the user that becomes the new PI, and the project_role_id that the current PI needs to be assigned if he stays on the project, if project_role_id is nil, that means that user should be deleted from the project.
This approach would eliminate validations altogether!
The first answer was a very fun exercise, and I hate to suggest someone change their entire approach, I'd rather just answer the question asked the best I can. But after seeing how complex the 'validations' became, I think it would be best not to rely on them in your case, you just have to CODE your app to insure your conditions are met.
HTH
Related
I have a somewhat complex Rails model setup that I'll try to simplify as much as possible. The goal of this setup is to be able to have objects (Person, Pet) that are long-lived, but with relationships between them changing each year via TemporalLink. Basically, I have these models:
class Person < ActiveRecord::Base
include TemporalObj
has_many :pet_links, class_name: "PetOwnerLink"
has_many :pets, through: :pet_links
end
class Pet < ActiveRecord::Base
include TemporalObj
has_many :owner_links, class_name: "PetOwnerLink"
has_many :owners, through: :owner_links
end
class PetOwnerLink < ActiveRecord::Base
include TemporalLink
belongs_to :owner
belongs_to :pet
end
and these concerns:
module TemporalLink
extend ActiveSupport::Concern
# Everything that extends TemporalLink must have a `year` attribute.
end
module TemporalObj
extend ActiveSupport::Concern
# Everything that extends TemporalObj must have a find_existing() method.
####################
# Here be dragons! #
####################
end
The desired behavior is:
When creating a TemporalObj (Pet, Person):
1) Check to see if there is an existing one, based on certain conditions, with find_existing().
2) If an existing duplicate is found, don't perform the create but still perform necessary creations to associated objects. (This seems to be the tricky part.)
3) If no duplicate is found, perform the create.
4) [Existing magic already auto-creates the necessary TemporalLink objects.]
When destroying a TemporalObj:
1) Check to see if the object exists in more than one year. (This is simpler in actuality than in this example.)
2) If the object exists in only one year, destroy it and associated TemporalLinks.
3) If the object exists in more than one year, just destroy one of the TemporalLinks.
My problem is I have uniqueness validations on many TemporalObjs, so when I try to create a new duplicate, the validation fails before I can perform any around_create magic. Any thoughts on how I can wrangle this to work?
You can (and should) use Rails' built-in validations here. What you've described is validates_uniqueness_of, which you can scope to include multiple columns.
For example:
class TeacherSchedule < ActiveRecord::Base
validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
end
http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of
In response to JacobEvelyn's comment, this is what I did.
Created a custom validate like so
def maintain_uniqueness
matching_thing = Thing.find_by(criteria1: self.criteria1, criteria2: self.criteria2)
if !!matching_thing
self.created_at = matching_thing.created_at
matching_thing.delete
end
true
end
Added it to my validations
validate :maintain_event_uniqueness
It worked.
First, thanks for taking the time to read. I'm new to Rails and have been stuck on this one for many hours.
In my Rails 3.2 app, I have three models: User, Organization, and Membership (the last is a join model between User and Organization).
When a user creates an organization, he/she should become a member upon create. So, in my Organization model, I've included a before_create callback that builds a Membership. The problem is that while the Membership builds when the new Organization is created, the user_id on the Membership object is set to "nil.," and therefore the current user is not a member.
Hardcoding in the user_id attribute in the callback actually does correctly build the membership, i.e. (:user_id => "1"), but in general asking the Organization model to be aware of current user state seems like bad MVC practice.
What's the proper way to set the current user ID on the new Membership? It seems like my associations should handle that, but I might be wrong.
Here are my models — I'm leaving out some validation lines for readability's sake. Thanks so much in advance.
user.rb
class User < ActiveRecord::Base
has_many :memberships
has_many :organizations, :through => :memberships
end
membership.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :organization
end
organization.rb
class Organization < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
accepts_nested_attributes_for :memberships, :allow_destroy => true
...
before_create :add_membership
protected
def add_membership
self.memberships.build
end
end
You are right in the fact that allowing your model to magically know about the current user is bad MVC practice. So you have to somehow pass the current user id during creation. you can do this in many ways ; for example in the controller :
def create
#organization = Organization.new( params[:organization] ) do |org|
org.memberships.build( user_id: current_user.id )
end
# save, etc.
end
Doing this in the controller is fine, but it would be better if your business logic would reflect the fact that a user creating an organization should automatically belong to it. You could override new and / or create on Organization (or create your own method if you fear overrides) :
def new( params = {}, options = {} )
creator = options.delete( :creator )
super( params, options ) do |org|
org.memberships.build( user_id: creator.id ) if creator
yield org if block_given?
end
end
passing the user is easy now :
def create
#organization = Organization.new(params[:organization], creator: current_user)
end
If you don't like this approach, or if you don't want to override new or create a specific factory method, you can also make something similar to nested_attributes :
attr_accessible :creator_id
def creator_id=( user_id )
memberships.build user_id: user_id
end
then in your view :
f.hidden_field :creator_id, current_user.id
optional :
with first approach, for additional clarity / ease of use, you can also create a method on User :
def new_organization( params = {}, options = {}, &block )
Organization.new( params, options.merge(creator: self), &block )
end
... ok, Organization is hardcoded here (bad !) but your workflow is now quite understandable :
def create
# we know at first glance that the user is responsible for the organization
# creation, and that there must be specific logic associated to this
#organization = current_user.new_organization( params[:organization] )
# etc
end
with a little more thinking, it should be possible to avoid hardcoding Organization into User (using an association extension for instance)
EDIT
To be able to setup a validation on membership's organization presence, you need to do this :
class Organization < ActiveRecord::Base
has_many :memberships, inverse_of: :organization
end
class Membership < ActiveRecord::Base
belongs_to :organization, inverse_of: :memberships
validates :organization, presence: true
end
Let's explain this :
inverse_of sets up your associations to be bidirectional. By default, associations are one-way, which means that when you do organization.memberships.first.organization, rails tries to load the organisation again because it does not know how to "climb back" the association. When using inverse_of, rails knows it does not have to reload the organization.
validates MUST be setup on organization and NOT on organization_id. This way the validator knows we're "climbing back" the association, it knows that organization is a "parent" record and that it's in the process of being saved - so it does not complain.
Theory :- after create of a record in customer bill, i am sending two sets of data two different models. one set of data is sent to ledger and one set of data is sent to ledger_line_item. the complexity is that after sending of data i want the ledger_id to be stored in ledger_line_item. the code is as follows
code :-
class CustomerBill < ActiveRecord::Base
after_create :creating_ledger_line_items, :creating_ledger_items
def creating_ledger_items
CustomerLedger.create(:customer_id =>self.customer_id,/*rest of attributes*/)
end
def creating_ledger_line_items
CustomerLedgerLineItem.create(:customer_id =>self.customer_id,/*rest of attributes*/)
end
end
in ledger i have written
class CustomerLedger < ActiveRecord::Base
after_save :update_record_line_items
def update_record_line_items
a = CustomerLedgerLineItem.find_by_customer_id(self.customer_id)
a.update_attributes(:customer_ledger_id => self.id)
end
end
the above code works fine without error but the ledger_id is not been posted in ledger_line_items. i am not able to determine why this error is happening? is there any other way i can achieve my goal of posting ledger_id in ledger_line_items after a bill is created?
Guidance Required. Thanking you in advance.
You can change your models something as follows.:
I am assuming you have Customer Model.
class Customer < ActiveRecord::Base
has_one :customer_ledger
has_many :customer_ledger_line_items, :through => :customer_ledger
accepts_nested_attributes_for :customer_ledger
end
class CustomerLedger < ActiveRecord::Base
has_many :customer_ledger_line_items
accepts_nested_attributes_for :customer_ledger_line_items
end
class CustomerBill < ActiveRecord::Base
belongs_to :customer
after_create :creating_ledger_items, :creating_ledger_line_items
def creating_ledger_line_items
cl = self.customer.customer_ledger.build(your_attributes)
cl.save!
end
def creating_ledger_items
cli = self.customer.customer_ledger.customer_ledger_items.build(your_attributes)
cli.save!
end
end
In case you want to create the models on an *after_create* hook, I'll explain what's the problem.
When you create a model in rails, and you have hooks like *after_create*, *before_update*, etc. all the updates happens in a Transaction, so if any of them throws an exception, nothing is updated.
In this case, within a Transaction, you are trying to get the ID of a CustomerLedger that doesn't exists yet, because since everything is within a Transaction, the record is not saved to the database until the transaction is executed, and thats the reason that on CustomerLedger#update_record_line_items, self.id is always nil.
Using the nested attributes proposed by codeit is probably the best solution to your problem, but if you feel that nested attributes its an advance topic, you can do something like:
class CustomerBill < ActiveRecord::Base
after_create :created_leder_data
def create_ledger_data
customer_ledger = CustomerLedger.build(customer_id: self.customer_id, # Rest of attributes)
customer_ledger.customer_ledger_line_items.build(customer_id: self.customer_id, # Rest of attributes)
customer_ledger.save!
end
end
What is the way to implement "business rules" in Rails?
Let us say I have a car and want to sell it:
car = Cars.find(24)
car.sell
car.sell method will check a few things:
does current_user own the car?
check: car.user_id == current_user.id
is the car listed for sale in the sales catalog?
check: car.catalogs.ids.include? car.id
if all o.k. then car is marked as sold.
I was thinking of creating a class called Rules:
class Rules
def initialize(user,car)
#user = user
#car = car
end
def can_sell_car?
#car.user_id == #user.id && #car.catalogs.ids.include? #car.id
end
end
And using it like this:
def Car
def sell
if Rules.new(current_user,self).can_sell_car
..sell the car...
else
#error_message = "Cannot sell this car"
nil
end
end
end
As for getting the current_user, I was thinking of storing it in a global variable?
I think that whenever a controller action is called, it's always a "fresh" call right? If so then storing the current user as a global variable should not introduce any risks..(like some other user being able to access another user's details)
Any insights are appreciated!
UPDATE
So, the global variable route is out! Thanks to PeterWong for pointing out that global variables persist!
I've now thinking of using this way:
class Rules
def self.can_sell_car?(current_user, car)
......checks....
end
end
And then calling Rules.can_sell_car?(current_user,#car) from the controller action.
Any thoughts on this new way?
I'd use the following tables:
For buyers and sellers:
people(id:int,name:string)
class Person << ActiveRecord::Base
has_many :cars, :as => :owner
has_many :sales, :as => :seller, :class_name => 'Transfer'
has_many :purchases, :as => :buyer, :class_name => 'Transfer'
end
cars(id:int,owner_id:int, vin:string, year:int,make:string,model:string,listed_at:datetime)
listed_at is the flag to see if a Car is for sale or not
class Car << ActiveRecord::Base
belongs_to :owner, :class_name => 'Person'
has_many :transfers
def for_sale?
not listed_at.nil?
end
end
transfers(id:int,car_id:int,seller_id:int,buyer_id:int)
class Transfer << ActiveRecord::Base
belongs_to :car
belongs_to :seller, :class_name => 'Person'
belongs_to :buyer, :class_name => 'Person'
validates_with Transfer::Validator
def car_owned_by_seller?
seller_id == car.owner_id
end
end
Then you can use this custom validator to setup your rules.
class Transfer::Validator << ActiveModel::Validator
def validate(transfer)
transfer.errors[:base] = "Seller doesn't own car" unless transfer.car_owned_by_seller?
transfer.errors[:base] = "Car isn't for sale" unless transfer.car.for_sale?
end
end
First, the standard rails practice is to keep all business logic in the models, not the controllers. It looks like you're heading that direction, so that's good -- BUT: be aware, there isn't a good clean way to get to the current_user from the model.
I wouldn't make a new Rules model (although you can if you really want to do it that way), I would just involve the user model and the car. So, for instance:
class User < ActiveRecord::Base
...
def sell_car( car )
if( car.user_id == self.id && car.for_sale? )
# sell car
end
end
...
end
class Car < ActiveRecord::Base
...
def for_sale?
!catalog_id.nil?
end
...
end
Obviously I'm making assumptions about how your Catalog works, but if cars that are for_sale belong_to a catalog, then that method would work - otherwise just adjust the method as necessary to check if the car is listed in a catalog or not. Honestly it would probably be a good idea to set a boolean value on the Car model itself, this way users could simply toggle the car being for sale or not for sale whenever you want them to ( either by marking the car for sale, or by adding the car to a catalog, etc. ).
I hope this gives you some direction! Please feel free to ask questions.
EDIT: Another way to do this would be to have methods in your models like:
user.buy_car( car )
car.transfer_to( user )
There are many ways to do it putting the logic in the object its interacting with.
I would think this would a prime candidate for using a database, and then you could use Ruby to query the different tables.
You might take a look at the declarative authorization gem - https://github.com/stffn/declarative_authorization
While it's pre-configured for CRUD actions, you can easily add your own actions (buy, sell) and put their business logic in the authorization_rules.rb config file. Then, in your controllers, views, and even models!, you can easily ask permitted_to? :buy, #car
I'm doing something similar with users and what they can do with photo galleries. I'm using devise for users and authentication, and then I set up several methods in the user model that determine if the user has various permissions (users have many galleries through permissions) to act on that gallery. I think it looks like the biggest problem you are having is with determining your current user, which can be handled quite easily with Devise, and then you can add a method to the user model and check current_user.can_sell? to authorized a sale.
Using ActiveRecord, I have an object, Client, that zero or more Users (i.e. via a has_many association). Client also has a 'primary_contact' attribute that can be manually set, but always has to point to one of the associated users. I.e. primary_contact can only be blank if there are no associated users.
What's the best way to implement Client such that:
a) The first time a user is added to a client, primary_contact is set to point to that user?
b) The primary_contact is always guaranteed to be in the users association, unless all of the users are deleted? (This has two parts: when setting a new primary_contact or removing a user from the association)
In other words, how can I designate and reassign the title of "primary contact" to one of a given client's users? I've tinkered around with numerous filters and validations, but I just can't get it right. Any help would be appreciated.
UPDATE: Though I'm sure there are a myriad of solutions, I ended up having User inform Client when it is being deleted and then using a before_save call in Client to validate (and set, if necessary) its primary_contact. This call is triggered by User just before it is deleted. This doesn't catch all of the edge cases when updating associations, but it's good enough for what I need.
My solution is to do everything in the join model. I think this works correctly on the client transitions to or from zero associations, always guaranteeing a primary contact is designated if there is any existing association. I'd be interested to hear anyone's feedback.
I'm new here, so cannot comment on François below. I can only edit my own entry. His solution presumes user to client is one to many, whereas my solution presumes many to many. I was thinking the user model represented an "agent" or "rep" perhaps, and would surely manage multiple clients. The question is ambiguous in this regard.
class User < ActiveRecord::Base
has_many :user_clients, :dependent => true
has_many :clients, :through => :user_client
end
class UserClient < ActiveRecord::Base
belongs_to :user
belongs_to :client
# user_client join table contains :primary column
after_create :init_primary
before_destroy :preserve_primary
def init_primary
# first association for a client is always primary
if self.client.user_clients.length == 1
self.primary = true
self.save
end
end
def preserve_primary
if self.primary
#unless this is the last association, make soemone else primary
unless self.client.user_clients.length == 1
# there's gotta be a more concise way...
if self.client.user_clients[0].equal? self
self.client.user_clients[1].primary = true
else
self.client.user_clients[0].primary = true
end
end
end
end
end
class Client < ActiveRecord::Base
has_many :user_clients, :dependent => true
has_many :users, :through => :user_client
end
Though I'm sure there are a myriad of solutions, I ended up having User inform Client when it is being deleted and then using a before_save call in Client to validate (and set, if necessary) its primary_contact. This call is triggered by User just before it is deleted. This doesn't catch all of the edge cases when updating associations, but it's good enough for what I need.
I would do this using a boolean attribute on users. #has_one can be used to find the first model that has this boolean set to true.
class Client < AR::B
has_many :users, :dependent => :destroy
has_one :primary_contact, :class_name => "User",
:conditions => {:primary_contact => true},
:dependent => :destroy
end
class User < AR::B
belongs_to :client
after_save :ensure_only_primary
before_create :ensure_at_least_one_primary
after_destroy :select_another_primary
private
# We always want one primary contact, so find another one when I'm being
# deleted
def select_another_primary
return unless primary_contact?
u = self.client.users.first
u.update_attribute(:primary_contact, true) if u
end
def ensure_at_least_one_primary
return if self.client.users.count(:primary_contact).nonzero?
self.primary_contact = true
end
# We want only 1 primary contact, so if I am the primary contact, all other
# ones have to be secondary
def ensure_only_primary
return unless primary_contact?
self.client.users.update_all(["primary_contact = ?", false], ["id <> ?", self.id])
end
end