Scope that cooperates with a has many relationship - ruby-on-rails

I have a User model that has_many documents and a Document model that belongs to a user, like this:
class User < ActiveRecord::Base
has_many :documents, dependent: :destroy
end
class Document < ActiveRecord::Base
belongs_to :user
end
And I have a boolean field on Documents named archived. I can access all the documents that belong to a user through:
#user = User.first
#user.documents.
But what I want to do is create a scope on the User model to display all the documents that belong to that user and have an archived value of true. I could just use a model method, but I would like to figure out how to scope it. Something along the lines of scope, -> {documents.where(archived: true)}. How would I do something like that with a has_many relationship.

One of the best qualities of ActiveRecord scopes is that they compose with each other, and with relationships. Add this scope inside Document like so:
class Document
scope :archived { where(archived: true) }
end
Then this code will work as you expect:
#user = User.first
#user.documents.archived
An additional technique to be aware of, as your scopes become more complex, is that you can create a shorthand for them in places of frequent access.
class User
def archived_documents
documents.archived
end
end

To kind of answer my own question, you can also use a has_many relationship with a where clause. This is in the User Model:
has_many archived_documents, -> { where(archived: true) }, class_name: 'Document'

Related

create a relationship between non User model and AhoyMessage

I am using Ahoy Email to track emails I send to my restaurants.
However I'dd like to create a relationship like so :
an AhoyMessage belongs_to a Restaurant
a Restaurant has_many AhoyMessages
so that I can access, for example:
ahoy_message.restaurant.phone_number
==> "+33612345678"
I know when I look at the docs that there is an easy way to do so with the User model, but I can only use my Restaurant model and hence the example in the doc does not work for my particular case.
The docs say it's polymorphic and you can use any model.
Try
class CouponMailer < ApplicationMailer
track user: -> { Restaurant.find_by(email: message.to.first) }
end
class Restaurant < ApplicationRecord
has_many :messages, class_name: "Ahoy::Message", as: :user
end
So you would still use ahoy_message.user.phone_number but the ahoy_message.user is a polymorphic association to a restaurant object.

Rails: how to get records based on user interests from my current associations?

I have this relationship:
class Trip < ActiveRecord::Base
belongs_to :user
acts_as_taggable
acts_as_taggable_on :activities
end
class User < ActiveRecord::Base
has_many :trips
has_many :user_interests
has_many :interests, through: :user_interests
end
class Interest < ActiveRecord::Base
has_many :user_interests
has_many :users, through: :user_interests
end
Users are able to select certain types of interests, and these Trips have activities tagged with them. I want to be able to get records of the Trips that has certain activities that the user are interested.
I've been testing some things out like this:
= Trip.all.to_json (sees all the Trip records) and = current_user.interests.to_son (sees what current user interested in) just to test if it works... and it does.
Is it possible to get only the trips that the users are interested in?
I assume Interest model has name attribute and now question is to find all trips to which user is interested for.
As you are using acts_as_taggable on gem, you can do something like -
Trip.tagged_with(user.interests.pluck(:name), on: :activities, any: true)
How about this query:
Trip.includes('user_interests').where('trip.user_id = user_interests.user_id').references(:user_interests)
This will give you all trips where users are interested.
So you want to find trips where trip->activities match user->interests?
Assuming your Interest model has name attribute, the following works:
Trip.includes(taggings: :tag).where(tags: {name: current_user.interests.pluck(:name)})
I will advise you to encapsulate this as method in your User model like the following:
class User > ActiveRecord::Base
#..
#..
def trips_interested_in
Trip.includes(taggings: :tag).where(tags: {name: self.interests.pluck(:name)})
end
end
So that you could call:
trips = #user.trips_interested_in
And/Or you could also add this as a scope in the Trip model like the following:
class Trip > ActiveRecord::Base
scope :where_user_is_interested_in, ->(user) { includes(taggings: :tag).where(tags: {name: user.interests.pluck(:name)}) }
#..
#..
end
So you could also call:
trips = Trip.where_user_is_interested_in(#user)

Deleting first occurrence from many to many collection entry at rails?

In many to many fields delete method is deleting all the occurrence of collection. Say I have:
class user < ActiveRecord::Base
has_and_belongs_to_many :cars
end
class car < ActiveRecord::Base
has_and_belongs_to_many :users
end
users and cars are many to many relationship, I have defined my users_cars table. Now user can have repetitive car entry as relation. For example:
Car: A,B,C
User: U1,U2,U3
U1=[A,B,C,A,A,A,B]
Which can be implemented using many to many relationship, the way I have implemented. BUT, at the time when I want to delete one of the car entries of user the problem occurs.
User.cars.delete(car) #deletes all occurrence of car
User.cars.delete_at(User.cars.find_index(video_card)) #delete_at does not exist
Now how to resolve this?
First of all, you can't call User.cars unless you have defined a class level method cars in your User model, but in this way, you would return all cars, and that - in no way - would make sense.
Second, delete_at is a method that works on Array objects, and expects an integer to be passed in. So as a little hack, you can turn your ActiveRecord::Associations object into an array, and then call delete_at method.
user = User.first
user.cars.to_a.delete_at(Car.last.id) # assuming that the last car belongs
# to the first user, something you would never do in actual
# production code.
Edit:
You can also try the following to achieve the same functionality:
user = User.first
user.cars.where("cars.id = ?", Car.first.id).first.delete
Edit 2:
For what you asked in comment, you can have a model for the table cars_users.
rails g model CarUser
class Car < ActiveRecord::Base
has_many :cars_users
has_many :users, through: car_users
end
class User < ActiveRecord::Base
has_many :cars_users
has_many :cars, through: car_users
end
class CarUser < ActiveRecord::Base
belongs_to :car
belongs_to :user
end
And now, you can do:
CarUser.where("car_id = ? AND user_id = ?", Car.first.id, User.first.id).first.delete

How do I pass an argument to a has_many association scope in Rails 4?

Rails 4 lets you scope a has_many relationship like so:
class Customer < ActiveRecord::Base
has_many :orders, -> { where processed: true }
end
So anytime you do customer.orders you only get processed orders.
But what if I need to make the where condition dynamic? How can I pass an argument to the scope lambda?
For instance, I only want orders to show up for the account the customer is currently logged into in a multi-tenant environment.
Here's what I've got:
class Customer < ActiveRecord::Base
has_many :orders, (account) { where(:account_id => account.id) }
end
But how, in my controller or view, do I pass the right account? With the code above in place when I do:
customers.orders
I get all orders for account with an id of 1, seemingly arbitrarily.
The way is to define additional extending selector to has_many scope:
class Customer < ActiveRecord::Base
has_many :orders do
def by_account(account)
# use `self` here to access to current `Customer` record
where(:account_id => account.id)
end
end
end
customers.orders.by_account(account)
The approach is described in Association Extension head in Rails Association page.
To access the Customer record in the nested method you just can access self object, it should have the value of current Customer record.
Sinse of rails (about 5.1) you are able to merge models scope with the othe model has_many scope of the same type, for example, you are able to write the same code as follows in the two models:
class Customer < ApplicationRecord
has_many :orders
end
class Order < ApplicationRecord
scope :by_account, ->(account) { where(account_id: account.id) }
end
customers.orders.by_account(account)
You pass in an instance of the class you have defined. In your case, you would pass in a customer and then get the account.
From the API http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Accessing the owner object
Sometimes it is useful to have access to the owner object when building the query. The owner is passed as a parameter to the block. For example, the following association would find all events that occur on the user's birthday:
class User < ActiveRecord::Base
has_many :birthday_events, ->(user) { where starts_on: user.birthday },
class_name: 'Event'
end
In your example it would be:
class Customer < ActiveRecord::Base
has_many :orders, ->(customer) { where(account_id: customer.account.id) }
end
I know this is old, but since no answer was accepted yet, I thought adding my views on the point would harm no one.
The problem is that whenever you pass a scope to a has_many relationship, passing the instance of the owner class as an argument is not only a possibility but it is the only possibility to pass an argument. I mean, you are not allowed to pass more arguments, and this one will always be the instance of the owner class.
So #RobSobers, when you
"get all orders for account with an id of 1, seemingly arbitrarily."
it is not arbitrary, you get all orders with th id of the customer you called the relation on. I guess your code was something like
Customer.first.orders(#some_account_which_is_ignored_anyway)
Seems like has_many relation was not meant to accept arguments.
Personally, I prefer the solution of #МалъСкрылевъ.

Making record available only to certain models in Rails 3

I have a weird design question. I have a model called Article, which has a bunch of attributes. I also have an article search which does something like this:
Article.project_active.pending.search(params)
where search builds a query based on certain params. I'd like to be able to limit results based on a user, that is, to have some articles have only a subset of users which can see them.
For instance, I have an article A that I assign to writers 1,2,3,4. I want them to be able to see A, but if User 5 searches, I don't want that user to see. Also, I'd like to be able to assign some articles to ALL users.
Not sure if that was clear, but I'm looking for the best way to do this. Should I just store a serialized array with a list of user_id's and have -1 in there if it's available to All?
Thanks!
I would create a join table between Users and Articles called view_permissions to indicate that a user has permission to view a specific article.
class ViewPermission
belongs_to :article
belongs_to :user
end
class User
has_many :view_permissions
end
class Article
has_many :view_permissions
end
For example, if you wanted User 1 to be able to view Article 3 you would do the following:
ViewPermission.create(:user_id => 1, :article_id => 3)
You could then scope your articles based on the view permissions and a user:
class Article
scope :viewable_by, lambda{ |user| joins(:view_permissions).where('view_permissions.user_id = ?', user.id) }
end
To search for articles viewable by a specific user, say with id 1, you could do this:
Article.viewable_by(User.find(1)).project_active.pending.search(params)
Finally, if you want to assign an article to all users, you should add an viewable_by_all boolean attribute to articles table that when set to true allows an article to be viewable by all users. Then modify your scope to take that into account:
class Article
scope :viewable_by, lambda{ |user|
joins('LEFT JOIN view_permissions on view_permissions.article_id = articles.id')
.where('articles.viewable_by_all = true OR view_permissions.user_id = ?', user.id)
.group('articles.id')
}
end
If an Article can be assigned to multiple Writers and a Writer can be assigned to multiple Articles, I would create an Assignment model:
class Assignment < AR::Base
belongs_to :writer
belongs_to :article
end
Then you can use has_many :through:
class Article < AR::Base
has_many :assignments
has_many :writers, :through => :assignments
end
class Writer < AR::Base
has_many :assignments
has_many :articles, :through => :assignments
end

Resources