Multiple scoped has_many through relationship in Rails 4 [duplicate] - ruby-on-rails

This question already has an answer here:
Losing an Attribute When Saving Through an Association w/ Scope (Rails 4.0.0)
(1 answer)
Closed 9 years ago.
I want to model such a relationship between the models User and Event.
Therefore I have started with the following classes:
class User < ActiveRecord::Base
...
end
class Attendance < ActiveRecord::Base
# with columns user_id and event_id
...
end
class Event < ActiveRecord::Base
has_many :attendances
has_many :users, :through => :attendances
...
end
So far everything is okay: I can assign users and access attendances. But now I want to bring the state into play, such that I can distinguish e.g. between "attending", "unexcused absent", ... users. My first try was:
class Event < ActiveRecord::Base
has_many :attendances
has_many :users, :through => :attendances
has_many :unexcused_absent_users, -> { where :state => 'unexcused' },
:through => :attendances,
:source => :user
...
end
(:source has to be specified since otherwise it would search for a belongs to association named 'unexcused_absent_users')
The problem here is, that the where-predicate is evaluated on table 'users'.
I am clueless how to solve this 'correctly', without introducing new join tables/models for every state. Especially since every user can be just in one state for every event, I think a solution with one Attendance-model makes sense.
Have you an idea, how to get this right?

You can simply narrow the scope to look at the correct table:
has_many :unexcused_absent_users, -> { where(attendances: {state: 'unexcused'}) },
:through => :attendances,
:source => :user
Evem better, add this scope to the Attendance model and merge it in:
class Attendance < ActiveRecord::Base
def self.unexcused
where state: 'unexcused'
end
end
class Event < ActiveRecord::Base
has_many :unexcused_absent_users, -> { merge(Attendance.unexcused) },
:through => :attendances,
:source => :user
end

I have found a workaround, but I still think, this is ugly.
class Event < ActiveRecord::Base
has_many :user_attendances, :class_name => 'Attendance'
has_many :users, :through => :user_attendances, :source => :user
has_many :unexcued_absent_user_attendances, -> { where :state => 'unexcused'}, :class_name => 'Attendance'
has_many :unexcused_absent_users, :through => :unexcued_absent_user_attendances, :source => :user
end
In general: For every state that I want, I have to introduce a new has_many relationship with a scope and on top of that and an according has_many-through relationship.

this might work for you?
class Event < ActiveRecord::Base
has_many :attendances
has_many :users, :through => :attendances
def unexcused_absent_users
User.joins(:attendances)
.where(:state => 'unexcused')
.where(:event_id => self.id)
end
end
in rails 3+ methods are basically the same as scopes, just less confusing (in my opinion), they are chainable
event = Event.find(xxxx)
event.unexcused_absent_users.where("name LIKE ?", "Smi%")

Related

Rails model relationships with custom names

EDIT
Simple relationship but I'm having an issue getting it to work. There is a User. User's have many Bounties. User's have many BountyVotes through Bounty. Bounties have BountyVotes. For readability, I call BountyVotes -> Votes in the Bounty class. I get a Name Error: uninitialized constant User::bounty_vote when trying to access bounty_interests from the User model.
A user can create a Bounty. Other user's can vote on the Bounty.
//User class
class User < ActiveRecord::Base
has_many :bounties
has_many :bounty_interests, :through => :bounties, :source => :votes
end
//Bounty class
class Bounty < ActiveRecord::Base
belongs_to :user
has_many :votes, :class_name => :bounty_vote
end
//Bounty Vote class
class BountyVote < ActiveRecord::Base
belongs_to :bounty
end
Had to change two things. First, thanks to shakerlxxv I needed to change my through to be plural.
has_many :bounty_interests, :through => :bounties, :source => :votes
Than I also had to change the way I referenced my class name.
has_many :votes, :class_name => 'BountyVote'
Your :through clause needs to reference the plural form:
has_many :bounty_interests, :through => :bounties, :source => :votes

Rails has_many :through and has_one :through associations

First I'm using Rails 3.1 from the 3-1-stable branch updated an hour ago.
I'm developing an application where I have 3 essential models User, Company and Job, Here's the relevant part of the models:
class User < ActiveRecord::Base
has_many :companies_users, class_name: "CompaniesUsers"
has_many :companies, :through => :companies_users, :source => :company
end
class Company < ActiveRecord::Base
has_many :companies_users, class_name: "CompaniesUsers"
has_many :employees, :through => :companies_users, :source => :user
has_many :jobs, :dependent => :destroy
end
class Job < ActiveRecord::Base
belongs_to :company, :counter_cache => true
end
class CompaniesUsers < ActiveRecord::Base
belongs_to :company
belongs_to :user
end
The code works just fine, but I have been wondering if it's possible to:
I want to link a job with an employer, so think of this scenario: A user John who's an employee at Example, he posted the job Rails Developer, so I want to access #job.employer and it should get me back the user John, in other words:
#user = User.find_by_name('john')
#job = Job.find(1)
#job.employer == #user #=> true
So I thought of two possible solutions
First solution
class Job
has_one :employer, :through => :employers
end
class User
has_many :jobs, :through => :employers
end
class Employer
belongs_to :job
belongs_to :user
end
Second solution
class Job
has_one :employer, :class_name => "User"
end
class User
belongs_to :job
end
Which route should I go? Is my code right ?
I have another question, how to get rid of the class_name => "CompaniesUsers" option passed to has_many, should the class be Singular or Plural ? Should I rename it to something like Employees ?
P.S: I posted the same question to Ruby on Rails: Talk
Unless I'm missing something, I'd suggest simply doing
class Job
belongs_to :employer, :class_name => "User"
end
class User
has_many :jobs
end
This would give you methods like
user = User.first
user.jobs.create(params)
user.jobs # array
job = user.jobs.first
job.employer == user # true
You'll need an employer_id integer field in your Jobs table for this to work.
Typically you want to name your pass through model:
company_user
Then you don't need this:
class_name: "CompaniesUsers"
Just make sure the name of your database table is:
company_users
What you have works for you, so that's great. I just find when I don't follow convention I
run in to trouble down the road.

Rails has many and belongs to one

I have a User model which has many projects and a Project model which can have many users, but also belongs to a single user (ie the user who created this project). It must belong to a User. It also allows a list of users to be associated with it, think collaboration.
With this in mind, my models look like this:
class User < ActiveRecord::Base
has_many :assigned_projects
has_many :projects, :through => :assigned_projects
end
class Project < ActiveRecord::Base
belongs_to :user
has_many :assigned_projects
has_many :users, :through => :assigned_projects
end
class AssignedProject < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
Now, when I want to create a new project through a User, this is how I would do it:
user = User.create(:name => 'injekt')
user.projects.create(:name => 'project one')
Now, I know that projects is provided through an AssignedProject join model, which is why project.user will return nil. What I'm struggling to get my head around is the best way to assign the project creator (which by the way doesn't need to be user, it could be creator or something else descriptive, as long as it is of type User).
The idea then is to create a method to return projects_created from a User which will select only projects created by this user. Where user.projects will of course return ALL projects a user is associated with.
Assuming this kind of association is fairly common, what's the best way to achieve what I want? Any direction is greatly appreciated.
Add a creator_id column to your projects table for the creator relationship, and then add the associations to the models:
class User < ActiveRecord::Base
has_many :assigned_projects
has_many :projects, :through => :assigned_projects
has_many :created_projects, :class_name => "Project", :foreign_key => :creator_id
end
class Project < ActiveRecord::Base
belongs_to :user
has_many :assigned_projects
has_many :users, :through => :assigned_projects
belongs_to :creator, :class_name => "User", :foreign_key => :creator_id
end
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
I wanted to add little improvement to design. We don't actually need intermediate model because it does not contain any extra column other than reference_ids hence HABTM association is best suited over here.
class User < ActiveRecord::Base
has_and_belongs_to_many :projects, :join_table => :assigned_projects
has_many :created_projects, :class_name => "Project", :foreign_key => :creator_id
end
class Project < ActiveRecord::Base
has_and_belongs_to_many :users, :join_table => :assigned_projects
belongs_to :creator, :class_name => "User", :foreign_key => :creator_id
end

Rails named_scope across multiple tables

I'm trying to tidy up my code by using named_scopes in Rails 2.3.x but where I'm struggling with the has_many :through associations. I'm wondering if I'm putting the scopes in the wrong place...
Here's some pseudo code below. The problem is that the :accepted named scope is replicated twice... I could of course call :accepted something different but these are the statuses on the table and it seems wrong to call them something different. Can anyone shed light on whether I'm doing the following correctly or not?
I know Rails 3 is out but it's still in beta and it's a big project I'm doing so I can't use it in production yet.
class Person < ActiveRecord::Base
has_many :connections
has_many :contacts, :through => :connections
named_scope :accepted, :conditions => ["connections.status = ?", Connection::ACCEPTED]
# the :accepted named_scope is duplicated
named_scope :accepted, :conditions => ["memberships.status = ?", Membership::ACCEPTED]
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships
end
class Connection < ActiveRecord::Base
belongs_to :person
belongs_to :contact, :class_name => "Person", :foreign_key => "contact_id"
end
class Membership < ActiveRecord::Base
belongs_to :person
belongs_to :group
end
I'm trying to run something like person.contacts.accepted and group.members.accepted which are two different things. Shouldn't the named_scopes be in the Membership and Connection classes?
However if you try putting the named scopes in the Membership and Connection classes then you get this error (because Person.find(2).contacts returns an array of Persons which doesn't have an 'accepted' method:
>> Person.find(2).contacts.accepted
NoMethodError: undefined method `accepted' for #<Class:0x108641f28>
One solution is to just call the two different named scope something different in the Person class or even to create separate associations (ie. has_many :accepted_members and has_many :accepted_contacts) but it seems hackish and in reality I have many more than just accepted (ie. banned members, ignored connections, pending, requested etc etc)
You answered your own question:
Shouldn't the named_scopes be in the Membership and Connection classes?
Yes, they should be. This will let you call them as you wanted. It's also logically where they belong.
If you want something on Person that checks both, you can do something like:
named_scope :accepted, :conditions => ["connections.status = ? OR memberships.status = ?", Connection::ACCEPTED, Membership::ACCEPTED]
Maybe you want this to be an AND? not sure.
I'm sure this is not the best way and I believe you can do this on person and group models, but I also believe the following will work for you:
# models
class Person < ActiveRecord::Base
has_many :connections
has_many :contacts, :through => :connections
has_many :memberships
has_many :groups, :through => :memberships
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships
end
class Connection < ActiveRecord::Base
belongs_to :person
belongs_to :contact, :class_name => "Person", :foreign_key => "contact_id"
named_scope :accepted, :conditions => ["status = ?", Connection::ACCEPTED]
end
class Membership < ActiveRecord::Base
belongs_to :person
belongs_to :group
named_scope :accepted, :conditions => ["status = ?", Membership::ACCEPTED]
end
# controller
# get person's accepted contacts
#person = Person.first
#person.connections.accepted.map(&:contact)
# get group's accepted members
#group = Group.first
#group.memberships.accepted.map(&:person)

Rails: Multiple "has_many through" for the two same models?

Can't wrap my head around this...
class User < ActiveRecord::Base
has_many :fantasies, :through => :fantasizings
has_many :fantasizings, :dependent => :destroy
end
class Fantasy < ActiveRecord::Base
has_many :users, :through => :fantasizings
has_many :fantasizings, :dependent => :destroy
end
class Fantasizing < ActiveRecord::Base
belongs_to :user
belongs_to :fantasy
end
... which works fine for my primary relationship, in that a User can have many Fantasies, and that a Fantasy can belong to many Users.
However, I need to add another relationship for liking (as in, a User "likes" a Fantasy rather than "has" it... think of Facebook and how you can "like" a wall-post, even though it doesn't "belong" to you... in fact, the Facebook example is almost exactly what I'm aiming for).
I gathered that I should make another association, but I'm kinda confused as to how I might use it, or if this is even the right approach. I started by adding the following:
class Fantasy < ActiveRecord::Base
...
has_many :users, :through => :approvals
has_many :approvals, :dependent => :destroy
end
class User < ActiveRecord::Base
...
has_many :fantasies, :through => :approvals
has_many :approvals, :dependent => :destroy
end
class Approval < ActiveRecord::Base
belongs_to :user
belongs_to :fantasy
end
... but how do I create the association through Approval rather than through Fantasizing?
If someone could set me straight on this, I'd be much obliged!
Keep your first set of code, then in your User Model add:
has_many :approved_fantasies, :through => :fantasizings, :source => :fantasy, :conditions => "fantasizings.is_approved = 1"
In your Fantasizing table, add an is_approved boolean field.

Resources