I'm new to Rails and ActiveRecord and need some help. Basically, I have 4 models: User, Property, PropertyAccount, and AccountInvitation. Users and Properties have a many to many relationship via PropertyAccounts. AccountInvitations have a user's email and a property_id.
What I want to happen is that after a user registers on my app, his user account is automatically associated with some pre-created Properties. What I don't know how to do is write the query to get the Property objects from the AccountInvitations and save them to the User object. Please see def assign_properties for my pseudo code. Any help is welcome, thanks so much!
class User < ActiveRecord::Base
has_many :property_accounts
has_many :properties, through: :property_accounts
after_create :assign_properties
# Check to see if user has any pre-assigned properties, and if so assign them
def assign_properties
account_invitations = AccountInvitations.where(email: self.email)
if account_invitations.any?
account_invitations.each do |i|
properties += Property.find(i.property_id)
end
self.properties = properties
self.save
end
end
end
class AccountInvitation < ActiveRecord::Base
belongs_to :property
validates :property_id, presence: true
validates :email, uniqueness: {scope: :property_id}
end
class Property < ActiveRecord::Base
has_many :account_invitations
has_many :property_accounts
has_many :users, through: :property_accounts
end
class PropertyAccount < ActiveRecord::Base
belongs_to :property
belongs_to :user
end
Thanks to #wangthony , I looked at the includes method on http://apidock.com/rails/ActiveRecord/QueryMethods/includes and tweaked one of their examples in order to get this to work. Here's the solution:
def assign_property
self.properties = Property.includes(:account_invitations).where('account_invitations.email = ?', self.email).references(:account_invitations)
self.save
end
I believe you can do this:
user.properties = Property.includes(:account_invitations).where(email: user.email)
user.save
Related
I want to change has_many association behaviour
considering this basic data model
class Skill < ActiveRecord::Base
has_many :users, through: :skills_users
has_many :skills_users
end
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, validate: true
has_many :skills_users
end
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
end
For adding a new skill we can easily do that :
john = User.create(name: 'John Doe')
tidy = Skill.create(name: 'Tidy')
john.skills << tidy
but if you do this twice we obtain a duplicate skill for this user
An possibility to prevent that is to check before adding
john.skills << tidy unless john.skills.include?(tidy)
But this is quite mean...
We can as well change ActiveRecord::Associations::CollectionProxy#<< behaviour like
module InvalidModelIgnoredSilently
def <<(*records)
super(records.to_a.keep_if { |r| !!include?(r) })
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
to force CollectionProxy to ignore transparently adding duplicate records.
But I'm not happy with that.
We can add a validation on extra validation on SkillsUser
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
validates :user, uniqueness: { scope: :skill }
end
but in this case adding twice will raise up ActiveRecord::RecordInvalid and again we have to check before adding
or make a uglier hack on CollectionProxy
module InvalidModelIgnoredSilently
def <<(*records)
super(valid_records(records))
end
private
def valid_records(records)
records.with_object([]).each do |record, _valid_records|
begin
proxy_association.dup.concat(record)
_valid_records << record
rescue ActiveRecord::RecordInvalid
end
end
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
But I'm still not happy with that.
To me the ideal and maybe missing methods on CollectionProxy are :
john.skills.push(tidy)
=> false
and
john.skills.push!(tidy)
=> ActiveRecord::RecordInvalid
Any idea how I can do that nicely?
-- EDIT --
A way I found to avoid throwing Exception is throwing an Exception!
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, before_add: :check_presence
has_many :skills_users
private
def check_presence(skill)
raise ActiveRecord::Rollback if skills.include?(skill)
end
end
Isn't based on validations, neither a generic solution, but can help...
Perhaps i'm not understanding the problem but here is what I'd do:
Add a constraint on the DB level to make sure the data is clean, no matter how things are implemented
Make sure that skill is not added multiple times (on the client)
Can you show me the migration that created your SkillsUser table.
the better if you show me the indexes of SkillsUser table that you have.
i usually use has_and_belongs_to_many instead of has_many - through.
try to add this migration
$ rails g migration add_id_to_skills_users id:primary_key
# change the has_many - through TO has_and_belongs_to_many
no need for validations if you have double index "skills_users".
hope it helps you.
Please tell me the way how to implement dynamic associative link, which is itself determined by the attribute model.
I have two engines(Tbitcoin, Tstripe) each of them have a table payment. The model User has pay_currency attribute, which is the managing.
class User < ActiveRecord::Base
has_many :payments, ~> { where "pay_currency = 'real'" } , class_name: Tstripe::Payment, foreign_key: :uid
has_many :payments, ~> { where "pay_currency = 'bitcoin'" } ,class_name: Tbitcoin::Payment, foreign_key: :uid
end
What are the ways to dynamically determine the engine using User.last.payments.create ?
I think that you need a regular method instead of has_many association which will find proper payments associated with the user according to pay_currency value. Example:
class User < ActiveRecord::Base
def payments
payment_class = case pay_currency
when "real"
Tstripe::Payment
when "bitcoin"
Tbitcoin::Payment
end
payment_class.for_user(self)
end
end
class Tstripe::Payment < ActiveRecord::Base
belongs_to :user
def self.for_user(user)
where(user_id: user.uid)
end
end
There are 2 tables. One is User(id, name, email) and the other is Student(id, who_id).
I wanna Use this way:
Student.find(id).name, Student.find(id).email
rather than:
User.find(student.who_id).name, User.find(student.who_id).email
to get data.
How should I do?
btw, I cannot change who_id to user_id for any reason.
class User < ActiveRecord::Base
end
class Student < ActiveRecord::Base
end
You can add name and email methods in your Student model, like so:
class Student < ActiveRecord::Base
belongs_to :user, class_name: :User, foreign_key: 'who_id'
def name
user.name
end
def email
user.email
end
end
You could also use Rail's delegate method to do the same thing in less code:
class Student < ActiveRecord::Base
belongs_to :user, class_name: :User, foreign_key: 'who_id'
delegate :name, to: :user
delegate :email, to: :user
end
And once you ge tthat working, rather than Student.find(id).name, Student.find(id).email (which will fetch the data from the database twice) you should instead do this:
student = Student.find(id) #single call to the database
# get the properties from the previous database call
student.name
student.email
I am trying to order by a field in a related model in Rails. All of the solutions I have researched have not addressed if the related model is filtered by another parameter?
Item model
class Item < ActiveRecord::Base
has_many :priorities
Related Model:
class Priority < ActiveRecord::Base
belongs_to :item
validates :item_id, presence: true
validates :company_id, presence: true
validates :position, presence: true
end
I am retrieving Items using a where clause:
#items = Item.where('company_id = ? and approved = ?', #company.id, true).all
I need to order by the 'Position' column in the related table. The trouble has been that in the Priority model, an item could be listed for multiple companies. So the positions are dependent on which company_id they have. When I display the items, it is for one company, ordered by position within the company. What is the proper way to accomplish this? Any help is appreciated.
PS - I am aware of acts_as_list however found it did not quite suit my setup here, so I am manually handling saving the sorting while still using jquery ui sortable.
You could use the includes method to include the build association then order by it. You just make sure you disambiguate the field you are ordering on and there are some things you should read up on here on eager loading. So it could be something like:
#items = Item.includes(:priorities).where('company_id = ? and approved = ?', #company.id, true).order("priorities.position ASC")
class Item < ActiveRecord::Base
has_many :priorities
belongs_to :company
def self.approved
where(approved: true)
end
end
class Priority < ActiveRecord::Base
belongs_to :item
end
class Company < ActiveRecord::Base
has_many :items
end
#company = Company.find(params[:company_id])
#items = #company.items.joins(:priorities).approved.order(priorities: :position)
If I've understood your question, that's how I'd do it. It doesn't really need much explanation but lemme know if you're not sure.
If you wanted to push more of it into the model, if it's a common requirement, you could scope the order:
class Item < ActiveRecord::Base
has_many :priorities
belongs_to :company
def self.approved
where(approved: true)
end
def self.order_by_priority_position
joins(:priorities).order(priorities: :position)
end
end
and just use: #company.items.approved.order_by_priority_position
A Contact has a User assigned to them:
class Contact < ActiveRecord::Base
...
belongs_to :user
...
end
The user model has a field I want to exclude any time a user object or objects are returned from db. One of the ways to make it work is to add a default scope:
class User < ActiveRecord::Base
...
has_many :contacts
...
default_scope select((column_names - ['encrypted_password']).map { |column_name| "`#{table_name}`.`#{column_name}`"})
end
So in console if I do:
User.first
The select statement and result set do not include 'encrypted_password'.
However, if I do:
c = Contact.includes(:user).first
c.user
they do. The default scope on the User model does not get applied in this case and the 'encrypted_password' field is shown.
So my question is why? And also, is there a clean way to specify what fields should be returned on related object(s)?
You should just be able to use the :select option on the belongs_to relationship. Something like this:
class Contact < ActiveRecord::Base
...
belongs_to :user, :select => [:id, :first_name, :last_name, :email]
...
end