I am trying to implement my own methods for habtm association between ActiveResource and ActiveRecord classes in my Rails app.
Here is my classes:
class Project < ActiveResource::Base
end
class Target < ActiveRecord::Base
has_and_belongs_to_many :projects
def project_ids
project_ids
end
def project_ids=(pids)
project_ids = pids
end
def projects
projects = []
pids = project_ids.split(",")
pids.each do |pid|
projects.push(Project.find(pid))
end
end
def projects=(projs)
pids = projs.collect(&:id)
project_ids = pids.join(",")
end
end
I also have join table projects_targets with two columns project_id and target_id.
This does not record the association value into the join table upon creation.
My Questions:
Is there any other approach to do this ?
Am I missing something in my association methods?
I'd really appreciate any help.
Thanks in advance for help!
My advice is to not fight the framework and use the built in methods. If you wanted to implement custom accessors, then you should switch to a has many through because it will give you a model ( of the join table ) that you can work with to set the association manually.
Start here: http://edgeguides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
You will not need the custom accessors project_ids or projects because rails will do the work magically.
You can easily do assignments like so:
some_target.projects << some_project
You don't even have to call some_target.save because the << operator is saving the association and writing the id's to the join table for you.
If you have an array of projects, or an active record relation ( like the result of a where clause) you can pass it in the same way
some_target.projects << array_of_projects
To remove the association, you can call destroy with an object like
some_target.projects.destroy a_specific_project
This won't destroy a_specific_project, but it will un-associate it.
Related
I am using the Pundit gem to role scope my application and have found some difficulties merging ActiveRecord queries. I am working with Rails 5.1.4.
See I have three models, lets say Classroom, Student and Exam with:
Classroom has_many :students & has_many :exams, through: :students
Student belongs_to: :classroom & has_many :exams
Exam belongs_to: :student
Each have a policy scope with varying queries that I want to merge together to ensure a user has access to the Exam model that's within the Student scope, that's also within the Classroom scope.
Pundit let us do that by passing whatever ActiveRecord relationship from the controller to the Scope class inside a scope variable.
With that in mind, my scope classes appear as follow:
class ClassroomPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see specific classes
scope.where(type: :exam).where(reviewer_id: user.id)
end
end
end
class StudentPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see students from classes he's allowed in
scope.joins(:classroom).merge(policy_scope(Classroom))
end
end
end
class ExamPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see/grade exams from students he supervised
scope.joins(:student).merge(policy_scope(Student))
end
end
end
All works well, if I do policy_scope(Exam), I do get all exams from students who where inside a reviewer's classroom.
The issue arises when passing an already joined query such as policy_scope(Classroom.find(params[:classroom_id]).exams).
ActiveRecord effectively generates a strange query which is unusable because both a_classroom.exams and our scope do a join on :student.
In addition, removing joins from our exam scope, giving us scope.merge(policy_scope(Student)), makes our call policy_scope(Classroom.find(params[:classroom_id]).exams) work while breaking policy_scope(Exam).
Is there a way to work around this so that both use cases work ?
Or is my approach to Pundit wrong ?
Is this the limit of ActiveRecord ?
Any help regarding this would be appreciated !
policy_scope is intended to be used from Controllers. For e.g. refer the examples given in section https://github.com/varvet/pundit#scopes and you should find that this method is being demonstrated to be used either from controllers or views.
However as per your code you are trying to use policy_scope in your scope classes (i.e classes extending ApplicationPolicy::Scope class) themselves. And I think I am understanding the intention behind doing so to reuse the already defined scopes. But in my opinion that's a wrong way to think about it.
Once inside YourScopeClass#resolve method you should use normal querying mechanism like you do elsewhere in your Rails application.
I don't have a complete know-how of the fields you have in your model. But based on the comments you have added in resolve method for a reviewer, you should do something like:
class StudentPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see students from classes he's allowed in
arel = scope.joins(:classroom)
arel = arel.where(classrooms: { type: : exam, reviewer_id: user.id })
arel
end
end
end
Then use it from controller like policy_scope(Student)
Similarly
class ExamPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see/grade exams from students he supervised
arel = arel.joins(student: [ :classroom ])
arel = arel.where(classrooms: { type: : exam, reviewer_id: user.id })
arel
end
end
end
Then use it from controller like policy_scope(Exam)
And if you want to reuse some queries then try creating some generic methods which can be passed the query represented by Pundit Policy Scope.
For e.g.
class MyClass
class << self
def accessible_students(arel:, user_id:)
arel.where(classrooms: { type: : exam, reviewer_id: user_id })
end
end
end
class StudentPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see students from classes he's allowed in
arel = scope.joins(:classroom)
arel = MyClass.accessible_students(arel: arel, user_id: user.id)
arel
end
end
end
class ExamPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see/grade exams from students he supervised
arel = arel.joins(student: [ :classroom ])
arel = MyClass.accessible_students(arel: arel, user_id: user.id)
arel
end
end
end
I hope you find this helpful.
I have a Business Model and a Category Model.
Business belongs_to multiple category. What I am trying to do is building the relation without help of a third join table.
In business table there is a string column which will hold the comma separated category_id.
So I am wondering is it possible to build the relation like that. Any wise comment and idea will be appreciated.
I think it would be easier to do it with just a method:
class Business < ActiveRecord::Base
def categories
#categories ||= Category.where(id: category_ids.split(','))
end
def category_ids=(ids)
# this is needed to reset the memoization, when you change the category ids
#categories = nil
super
end
end
I need to map a non-existing table to ActiveRecord (in my case: the Wordpress Database Schema), which consists of a custom SQL statement as described below. I want to be able to use find(), first(), all() among other statements. Is there a way to accomplish this without actually overwriting all the finder methods? (i am currently redefining those methods, to accomplish similar results, but would love to know if there is a more ActiveRecord/Rails way to do so)
currently i am doing for example
class Category < ActiveRecord::Base
self.table_name="wp_terms"
def self.all
Category.find_by_sql("select distinct(wp_terms.term_id),wp_terms.name,wp_term_taxonomy.description,wp_term_taxonomy.parent,wp_term_taxonomy.count from wp_terms inner join wp_term_relationships ON wp_term_relationships.object_id = wp_terms.term_id INNER JOIN wp_term_taxonomy ON wp_term_taxonomy.term_id = wp_terms.term_id where taxonomy = 'category';")
end
end
thanks for any pointers.
You can use a default scope:
class Category < ActiveRecord::Base
self.table_name="wp_terms"
# Individual scopes for readability - the names could be improved.
scope :select_scope, select("DISTINCT(wp_terms.term_id), wp_terms.name, wp_term_taxonomy.description, wp_term_taxonomy.parent, wp_term_taxonomy.count")
scope :join_scope, joins("INNER JOIN wp_term_relationships ON wp_term_relationships.object_id = wp_terms.term_id INNER JOIN wp_term_taxonomy ON wp_term_taxonomy.term_id = wp_terms.term_id")
scope :where_scope, where("taxonomy = 'category'")
default_scope select_scope.join_scope.where_scope
end
Then you should be able to call any finder method on Category without having to implement it yourself.
Would you consider creating a view-backed model as outlined in Enterprise Rails?
I have 3 models: Category, Account, and SubAccount
The relations are:
Accounts has_many :sub_accounts
Categories has_many :sub_accounts
I wanted to get a list of all Categories that are not used by a given account.
My method in the Category model currently looks like:
class Category < ActiveRecord::Base
def self.not_used_by(account)
Category.find_by_sql("select * from categories where id not in(select category_id from sub_accounts where account_id = #{account.id})")
end
end
My question is, is there a cleaner alternative than using SQL?
NB. I am currently using Rails 3(beta)
You could move the method to the account model and use more of ActiveRecord by doing something like:
class Account < ActiveRecord::Base
def unused_categories
Category.where("id NOT IN (?)", sub_accounts.map(&:category_id))
end
end
Then you could do something like:
Account.first.unused_categories
AR does not do this out of the box. You may also want to check the excellent SearchLogic gem for a programatic approach.
search = Category.search
search.id_not_in sub_accounts.map(&:category_id)
search.name_equals "auto"
search. ..# other conditions
search.all
Try MetaWhere. http://metautonomo.us/projects/metawhere
You'll need my fork of Arel installed until the changes get merged (soon!) but with it installed you can do something like:
Category.where(:id.not_in => sub_accounts.map(&:category_id))
I have two models Library and Book. In my Library model, I have an array - book_ids. The primary key of Book model is ID.
How do I create a has_many :books relation in my library model?
This is a legacy database we are using with rails.
Thanks.
Your database schema doesn't really conform with the prescribed Rails conventions so you will probably have a hard time making the default has_many association work. Have you tried fiddling with the custom SQL options with it thought?
If you can't get the built in has_many association to work, you'll have to roll your own. I would define the books and books= methods on your Library model, and inside them set a virtual attribute, which you then save as an array in the database. Perhaps something like this:
class Book > ActiveRecord::Base; end
class Library > ActiveRecord::Base
before_save :serialize_books
def books
#books || nil
end
def books=(new_books)
#books = new_books
end
private
def serialize_books
#attributes['books'] = "[" + #books.collect {|b| b.id }.join(',') + "]"
end
end
That up there wouldn't pull out the dataIf you wanted to go even more gung ho and support single query find operations, you could use some custom SQL in a scope or override find and add it to the default options. Comment if you want help with any of this!
If you want to use has_many you could use the options :counter_sql and :finder_sql using the MySQL LIKE or REGEX syntax. But its probably better to first load the Libary model, then parse the book_ids column and load the books, or directly build a query with that string.
Consider using :serialize method with ActiveRecord:
http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002284
it might do what you want