I have problem with defining abilites when resources are deeply nested. I have these classes: Teacher, Division, Student, Absence and User (Teacher and Student belongs to Devise User model):
#Teacher
has_many :divisions
#Division
belongs_to :teacher
#Student
belongs_to :division
has_many :absences
#Absence
belongs_to :student
There is no problem when I want to ensure that Teacher can manage only Students that belongs to his division:
#This works
if user.teacher?
can :manage, Student, division: { teacher_id: user.teacher.id }
end
Problem occurrs when I want to ensure that Teacher can manage Absences that belongs to Students from his divisions:
#This doesn't work and returns PG::Error: ERROR: column students.divisions does not exist
can :manage, Absence, student: { division: { teacher_id: user.teacher.id } }
Any suggestions for defining ability for this nested resources?
This should work:
if user.teacher?
can :manage, Absence do |absence|
absence.student.division.teacher_id == user.teacher.id
end
end
The cancan wiki: Defining Abilities with Blocks
Related
Ruby on Rails app with ActiveRecord and Postgres database. I have this relation of two models with a join table in between, basically every user can like multiple movies:
class Movie
has_many :user_movie_likes
has_many :users, through: :user_movie_likes
end
class UserMovieLike
belongs_to :movie
belongs_to :user
end
class User
has_many :user_movie_likes
has_many :movies, through: :user_movie_likes
end
Considering I have the user id, I can easily fetch all the movies that are liked by this user, something like:
# movie.rb
scope :liked, ->(user_id) { joins(:user_movie_likes).where('user_movie_likes.user_id' => user_id) }
But I can't figure out how to do the inverse of this - all the movies that the given user didn't like yet. If I do where.not('user_movie_likes.user_id' => user_id), it will still return movies that the user liked, because some other user liked them, and will not return movies that have no likes at all. For now I'm just plucking the ids from the liked scope and do where.not(id: liked_ids), but I'm sure there is a good way to do it in one query. I found some other questions similar to this one on StackOverflow, but they were different cases, i.e. finding movies that have no likes etc.
Try the below:
scope :liked, lambda { |user_id|
joins("LEFT OUTER JOIN user_movie_likes ON user_movie_likes.movie_id = movies.id AND user_movie_likes.user_id = #{user_id}")
.where('user_movie_likes.id IS NULL')
}
I have three models:
Invitation.rb
belongs_to :department
Department.rb
belongs_to :organisation
has_many :invitations
Organisation.rb
has_many :departments
Organization table has tag field.
I need to write a query to search for Invitations, for the organization of which there is a tag field with the content 'First tag'
Something like this:
Invitation.joins(:department).joins(:organisation).where(tag: 'First tag')
But I don't know how to build such a query.
Invitation.joins(department: :organisation).where(organisations: { tag: 'First tag' })
Why? Because to "reach" the organisations table from invitations you can use the relationship between the departments table.
Table.joins(...).joins(...)
Is "joining" twice the same table, what you need is to reference the relatinoship between departments and organisations. That you can do it as:
{ department: :organisation }
Passing that to join loads a JOIN clause in the receiver with departments and in departments with organisations.
Notice the where clause can be used as { organisations: { tag: ... } } or ["organisations.tag = ?", ...], they can be used interchangeably. What you use is just a matter of preference yours or from your colleagues.
To do what you are trying to do without changing your models you should pass a hash to the joins method like so:
Invitation.joins(department: :organisation).where(organisations: { tag: 'First tag' })
This tells ActiveRecord to use the department association as defined on the Invitation model and the organisation association from the Department model
To make your life a bit easier you can add a has_many through association like this:
class Invitation < ApplicationRecord
belongs_to :department
has_one :organisation, through: :department
end
class Department < ApplicationRecord
belongs_to :organisation
has_many :invitations, inverse_of: :department
end
class Organisation
has_many :departments, inverse_of: :organisation
has_many :invitations, through: :departments
end
Now you can use the invitations relation on Organisation as follows:
Invitation.joins(:organisation).where(tag: 'First tag')
To see the sql (e.g. in the console) you can use the #to_sql method:
Invitation.joins(:organisation).where(tag: 'First tag').to_sql
Finally, if you use a scope (and dont like the hash in the where method in which you have to specify the table) you can use a "scope" and merge as follows:
class Organisation
class << self
def just_tagged
where(tag: 'First tag')
end
end
end
Now you can refer to the scope within your query using merge:
Invitation.joins(:organistion).merge(Organisation.just_tagged)
In my app (Rails 4.2.0.rc2), users can be either students or admins of a given institution. There's an association :admin_institutions on User that returns all the institutions the user is an admin of by checking their role in the join table. There's also an association :students on Institution that returns all the users who are students at that institution, again according to institution_users.role.
These associations work as expected, so I added an association :admin_students to User, meant to return all the students at all the institutions for which a given user is an admin.
class InstitutionUser < ActiveRecord::Base
belongs_to :institution
belongs_to :user
end
class Institution < ActiveRecord::Base
has_many :institution_users
has_many :users, :through => :institution_users
has_many :students, -> { where "institution_users.role = 'Student'" }, :through => :institution_users, source: :user
...
end
class User < ActiveRecord::Base
has_many :institution_users
has_many :admin_institutions, -> { where "institution_users.role = 'Admin'" }, through: :institution_users, source: :institution
has_many :admin_students, through: :admin_institutions, source: :students
...
end
However, :admin_students does not work as expected. It generates the following SQL:
SELECT "users".* FROM "users" INNER JOIN "institution_users" ON "users"."id" = "institution_users"."user_id" INNER JOIN "institutions" ON "institution_users"."institution_id" = "institutions"."id" INNER JOIN "institution_users" "institution_users_admin_students_join" ON "institutions"."id" = "institution_users_admin_students_join"."institution_id" WHERE "institution_users_admin_students_join"."user_id" = $1 AND (institution_users.role='Student') AND (institution_users.role = 'Admin') [["user_id", 190]]
Instead of looking for all the institutions where the user is an admin and selecting all their students, it seems to be looking for institutions where the user is BOTH a student and an admin, so it returns an empty collection.
Is there a way to write an association (as opposed to just a method) that will give me the results I want, without my conditions conflicting like this?
(Side note: Is this the expected behavior for this kind of association? If so, I'd really appreciate further insight into why ActiveRecord interprets it the way it does.)
This may not be the answer, but maybe it will lead to one.
I'm not a fan of the associations with hard-coded SQL:
-> { where "institution_users.role = 'Student'" }
They are definitely at least part of the problem because they cannot be interpreted by ActiveRecord to determine which table alias for institution_users to apply it to.
You can allow ActiveRecord that flexibility by referencing a class method of the InsitutionUser model:
def self.students
where(role: "Student")
end
This also keeps the InstitutionUser logic all in one place.
Then the association becomes:
has_many :students, -> {merge(InstitutionUser.students)}, :through => :institution_users, source: :user
Perhaps try it with this and see if that sorts it out for you, and if not it might get things going in the right direction.
I have a sql-Statement and I'd like to "convert" it into rails (activerecord) method calls.
This is my query
'SELECT * FROM clients WHERE company_id IN (SELECT company_id FROM companies_projects WHERE project_id= ? )
companies_projects is a join table for an n:n relation of companies and projects
clients belong to companies (1:n)
project is an external resource and has no has_many companies, so I can't go from that direction
I want to get all clients that belong to companies that belong to one project, so I can list them in the index-page
My models
class Client < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :companies_projects
has_many :clients
has_many :projects, :through => :companies_projects
end
I checked the statement in rails console and it works.
I have two problems impelementing this query.
1. find_by_sql
I tried this method
Client.find_by_sql('SELECT * FROM clients WHERE company_id IN (SELECT company_id FROM companies_projects WHERE project_id= ? )',project.id)
But it throws an InvalidStatement Exception, MySQL Syntax Error near "?"
I also tried to put the sql and bindings into an array [sql,bind1], that works but I get an array and need an ActiveRecordRelation
2. where
I'm new to rails and can't figure out a valid method chain for such a query.
Could someone point me in the right direction?
I would prefer using ActiveRecord methods for the query, but I just don't know which methods to use for the nested selects.
You should have following associations between your models:
class Client < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_and_belongs_to_many :projects
has_many :clients
end
class Project < ActiveRecord::Base
has_and_belongs_to_many :companies
has_many :clients, through: :companies
end
Then it is simply:
project.clients
Client.where(company_id: CompanyProject.where(project_id: project.id).pluck(:id))
Or you can use JOIN
Client.joins(:company_project).where('companies_projects.project_id = ?', project.id)
But the best solution was proposed by #arup-rakshit
Considering that you have an intermediate model CompanyProject, this can be achieved with following query:
Client.where(:company_id => CompanyProject.where(:project_id => project_id).map(&:company_id) )
[Edit: made company_id a symbol)
I have users and companies in a many to many relationship by a join table which has a column for user Role. I'm not sure if this is the best way to have the model set up.
Now each user can have different roles depending on the company, what is the best way to design and access user's role using ActiveRecord associations?
I would like to return via JSON the user's role based on their current company and default to something if their company is nil OR their role has not been set (nil).
Update:
What I've got now after reading Many-to-many relationship with the same model in rails? which is a bit different (many to many on itself).
CompaniesUser
belongs_to :company
belongs_to :user
Company
has_many(:companies_users, :dependent => :destroy)
has_many :users, :through => :companies_users
User
has_one :company
has_many(:companies_users, :dependent => :destroy)
has_many :companies, :through => :companies_users
Appreciate any advice as I'm just starting to learn this!
What you have above is correct, in terms of the ActiveRecord relationships. If you'd like to read more on the subject I believe this is the best source: http://guides.rubyonrails.org/association_basics.html
One problem I see there is that CompaniesUsers should be in singular form: CompanyUser, and then in all cases where you use :companies_users use: :company_users
I am assuming here that the current company of the User is the last one assigned.
Now in order to serialize in JSON format you should add the following in your User ActiveRecord:
def serializable_hash(options = nil)
options ||= {}
h = super(options)
if(defined?self.company_users.last and defined?(self.company_users.last).role)
h[:role] = (self.company_users.last).role
else
h[:role] = 'default_value'
end
end