How do I write a scope in Rails? - ruby-on-rails

I am trying to learn how to write scopes in Rails.
I have models for user, organisation and organisation request. The associations are:
User
has_one :organisation
Organisation
belongs_to :owner, class_name: 'User'
has_many :organisation_requests
Organisation_request
belongs_to :organisation
In my organisation request model, I'm trying to write a scope to pick out all the organisation requests that belong to the organisation's owner.
scope :same_org, where(:organisation_id => owner.organisation_id)
Then in my organisation_requests controller, I have:
def index
#organisation_requests = OrganisationRequest.same_org
end
I tried the above. Owner is an alias for user. In each organisation, one user is nominated as that organisation's owner. I want that user to see an index of the organisation requests that come in for that owner's organisation.
Can anyone see what I've done wrong here? I'm not understanding something about how to write scopes.

In your model, try this:
scope :same_org, -> {where(:organisation_id => owner.organisation_id) }

The upvoted answer is wrong (nothing personal, #Hasmukh Rathod) - there is no organisation_id column in users table (it's actually vice versa - there is a user_id column in organisations table).
I would suggest the following solution:
scope :same_org, ->(owner_id) { joins(organisation: :user).where(users: { id: owner_id }) }
Having the above scope, what you'll need to do, is to pass the owner_id as an argument. I'm sure there is a way to get it working without passing an argument to scope, but I'm not yet sure how (I've just woke up :D).
So example:
owner_id = User.find(1).id
OrganisationRequest.same_org(owner_id) # would give you the expected collection of requests

Related

Rails 5 - using a scope in an edit action to find relevant children of a specific instance

I am trying to learn how to use scopes in my Rails 5 app.
I have asked a background question here.
have models in my Rails 5 app for User, Proposal and Potential.
Users create Proposals which they themselves and others can then create comments on.
The associations between models are:
User
has_many :proposals, dependent: :destroy
has_many :potentials
Proposal
belongs_to :user
has_many :potentials, inverse_of: :proposal
accepts_nested_attributes_for :potentials, reject_if: :all_blank, allow_destroy: true
Potential
belongs_to :proposal, inverse_of: :potentials
belongs_to :user
In my routes file, I have two resources for potentials. I'm not sure if I've gone off piste with this bit- I cant find an example of how to do this otherwise. I have both:
resources :potentials
as well as:
resources :proposals do
resources :potentials
Objective:
When the user who made the proposal tries to edit it, I only want that user to be able to edit the potentials that they created themselves.
The reason I have two routes set up for potentials is that the nested resource has a nested form fields inside the proposal form, so that the proposal creator can make a potential in that way. Any other user that sees the proposal and makes a potential does it via a separate form.
Any user (including the proposal creator, can edit the potential via that separate form), and the proposal creator can also edit any of its own proposals by the nested form in the proposal form.
At the moment, whenever I edit the proposal form (even when I don't edit the potential nested fields), all of the potentials are updated to insert the proposal creator's user id overriding the actual potential creator's user id.
Solution
I am trying to limit the edit action in the proposals controller, so that it only allows the proposal /potentials to be edited if they have the user_id == the proposal.user_id.
For this purpose, I have written scopes in my proposal.rb
scope :owner_potentials, ->{ where(user_id: potential.user_id ) }
scope :third_party_potentials, ->{ where(user_id: != potential.user_id) }
The solution in the post i liked above was to try using a scope. Since scopes are meant to work on the class, rather than an instance, I'm stuck in trying to figure out how to adapt them so that I can use the scope to search for all the compliant potentials (i.e. potentials where potential.user_id == proposal.user_id). That means Im not searching the Proposal class, Im searching the specific proposal.
This post suggested defining Event.all inside the relevant controller action, but then how would I limit that so it only applied to the specific potentials edit line? I have other lines in my edit action which should not be tested on the Proposal table, but just the instance. If this were able to work, I imagine I would then need to rewrite my scope to try to exclude all the other proposals.
Is there a way to use an edit action in a controller with a scope, on a specific instance?
I would suggest scopes like this:
scope :owner_potentials, -> (user_id) { where(user_id: user_id) }
scope :third_party_potentials, -> (user_id) { where.not(user_id: user_id) }
When calling these scopes you just need to pass current user's id.
Scopes define queries for the AR class they are defined in. You say you have written owner_potentials and third_party_potentials scopes in proposal.rb. But if these scopes are meant to return a collection of potentials, then these should be defined in the Potential class. If you need to access these scopes from a proposal record, you can chain scopes to associations, e.g.
class Potential
scope :owner_potentials, -> (user) { where(user: user) }
scope :third_party_potentials, -> (user) { where.not(user: user) }
end
...
class ProposalsController # Proposals::PotentialsController..? imo Proposals::PotentialsController#edit sounds like an endpoint for editing exactly one potential record and nothing else, which doesn't sound like what you want. Your call on how to structure the controller/routes though.
def edit
#proposal = ... # Whatever your logic is to find the proposal
#proposal.potentials.owner_potentials(current_user) # do something with the user's potentials
#proposal.potentials.third_party_potentials(current_user) # do something with the potentials the user doesn't own
end
end
You can see here how you chain an association (.potentials) to a scope (.owner_potentials).
Also, if you have an association, you can treat that association as a field in a where method, a la where(user: user) instead of where(user_id: user.id).
Last thing to note is that you probably want to change the name of the scopes with this refactor.
potentials.owner_potentials(user) is a bit redundant. Maybe something like potentials.owned_by(user) ?

Rails associations - orders

So I have been trying to create a dummy application to try and learn Rails. The app I thought I could create is a coffee ordering app for a group of people in work.
So the website will have many users.
A user can create a coffee_order.
A coffee order contains orders for other individual users.
Each user can have one or more coffee_shop_items (e.g. latte,
cappuccino,danish, muffin, etc)
A coffee order also has an assignee, this is the person who is tasked
with going and getting the order.
So as a user, I create a coffee order, select an assignee, add users to the order, and add one or more coffee shop items to each user,
I am really struggling with how the database should be, and what the associations need to be, along with any join tables?
I am also trying to use nested attributes for the form entry.
Thanks in advance for help.
Update with some code I have tried to create a coffee order:
#coffee_order = CoffeeOrder.new(coffee_order_params)
params[:coffee_order][:user_coffee_orders_attributes].each do |user_order|
order = #coffee_order.user_coffee_orders.new(user_id: user_order[1][:user_id].to_i)
user_order[1][:coffee_shop_items].each do |item|
coffee_shop_item = CoffeeShopItems.find(item) if item != ""
# this line fails! see error below
#coffee_order.user_coffee_orders.coffee_shop_items << coffee_shop_item if coffee_shop_item != nil
end
end
error:
NoMethodError (undefined method `coffee_shop_items' for #<UserCoffeeOrder::ActiveRecord_Associations_CollectionProxy:0x42c6180>):
The coffee_shop_items belong to the order, not the user. After all, a user could probably create another order another day? You should probably also check out the rails documentation, which, IIRC actually contains a walk-through of a shopping cart application.
User has_many :coffes_orders
User has_many :coffee_orders_he_needs_to_get, class_name: "CoffeeOrder", foreign_key: "assignee_id"
CoffeeOrder belongs_to :user
CoffeeOrder belongs_to :assignee, class_name: "User"
CoffeeOrder has_and_belongs_to_many :coffee_shop_items
Coffee_shop_items has_and_belongs_to_many :coffee_orders

Conditional associations or scope?

Let's say I have two models
class Client < ActiveRecord::Base
has_many :accounts
end
class Account < ActiveRecord::Base
belongs_to :client
end
Now, I want to have convenient method to access approved accounts(:approved => true).
What is better to use, and why?
has_many: approved_accounts, -> { where :approved => true } for Client model
or
scope: approved, -> { where :approved => true } for Account model
Short answer, It Depends. For Long answer, kindly read on...
Conditional associations allows us to Customize the query that ActiveRecord will use to fetch the association. Use when you are sure that this condition is permanent & you will never-ever need access to data that doesn't fit the conditional(atleast not in this model) as Conditional associations are APPLIED to every query to ActiveRecord does to the association from that particular model.
Scope It is basically, a class method for retrieving and querying objects. so what you are actually doing is defining the following method in your model.
class Client < ActiveRecord::Base
self.approved
where(:approved => true)
end
end
so scope is generally use to define short names for one or more regularly used query customization. But important difference is that scope is not auto-applied unless you use default_scope but conditional associations are auto-applied.
In your case, you want to show unapproved accounts in this model? If not, then use conditional association. Else use scope
According to me, the scope solution seems the better:
As you probably know, to get the approved accounts of a client, you can do: approved_accounts = client.accounts.approved which is not less legible than: approved_accounts = client.approved_accounts
So not many difference here. But if in the future you want the list of all approved accounts (for statistics or whatever), with the scope solution, a single approved_accounts = Account.approved will be enough. But if you choose the client side, it will be trickier to get (understand: you will have to use the Client model).
Just consider than being approved is a property of an Account more than a property of a Client and it should be clearer that the scope is the best solution.
Hope this clarifies things.

Creating a fake has_many relationship - one that depends on some other member var

My application has a user model with some simple access level checks. This access level determines the scope of access to the other models in the database. To be precise, I have District, School, Teacher, Room and Student models. An Admin can see all records, a District can see all child schools, teachers, rooms and students, a principal can see all child teachers, rooms and students, a teacher can see all child rooms and students.
This is done by associating a User object with one or more levels of models.
belongs_to :district
belongs_to :school
belongs_to :teacher
So a school principal would have a district id and a school id, but its teacher id would be null.
Access to the children is controlled via functions like this:
def teachers
if is_admin?
Teacher.all
elsif is_district_head?
district.teachers
elsif is_principal?
school.teachers
else
[ teacher ]
end
end
This func is treated in code as if it were a plain old has_many relationship, where we can do stuff like:
current_user.teachers.find param[:teacher_id]
current_user.teachers.each {|t| puts t.id }
Whether the current_user is an admin or a teacher or anything in between, the correct amount of teachers is returned.
Except, that's sadly not the case. Only the actual has_many relationships work fully, my fake ones fail when I try to use .find or some function that's specific to the ActiveRecord collections created by has_many.
So, on to my question. How can I return an ActiveRecord collection object without explicitly calling the has_many function?
If you think I'm a bleedin' retard and I'm missing something godawfully obvious, please don't hesitate to enlighten me! I had thought this system was going great, until I had to use a .find off an administrator level user account. It was essentially running
Teacher.all.find :conditions => 'xyz'
...and that sadly returns an Enumerator object instead of an ActiveRecord for the Teacher model.
Basically you have four roles (Admin, District Head, Principal, and Teacher(?)). Each user has one of these roles, and you have a set of rules to determine the access privileges for each role. Correct?
This problem has been solved. I would urge you to consider an authorization system such as CanCan or Declarative Authorization. Both contain straightforward ways to handle the issues you are struggling with here. There's a learning curve, or course, but the time will be well spent.

What is the best way to do scoped finds based on access control rules in Rails?

I need to find an elegant solution to scoped finds based on access control rules. Essentially I have the following setup:
Users
Customers
AccessControl - Defines which user has access to another users data
Users need to be able to access not just their own customers but also shared customers of other users.
Obviously something like a simple association will not work:
has_many :customers
and neither will this:
has_many :customers, :conditions => 'user_id in (1,2,3,4,5)'
because the association uses with_scope and the added condition is an AND condition not an OR condition.
I also tried overriding the find and method_missing methods with the association extension like this:
has_many :customers do
def find(*args)
#get the user_id and retrieve access conditions based on the id
#do a find based on the access conditions and passed args
end
def method_missing(*args)
#get the user_id and retrieve access conditions based on the id
#do a find based on the access conditions and passed args
end
end
but the issue is that I don't have access to the user object / parent object inside the extension methods and it just does not work as planned.
I also tried default_scope but as posted here before you can't pass a block to a default scope.
Anyhow, I know that data segmentation and data access controls have been done before using rails and am wondering if somebody found an elegant way to do it.
UPDATE:
The AccessControl table has the following layout
user_id
shared_user_id
The customer table has this structure:
id
account_id
user_id
first_name
last_name
Assuming the the following data would be in the AccessControl table:
1 1
1 3
1 4
2 2
2 13
and so on...
And the account_id for user 1 is 13 I need to be able to retrieve customers that can be best described with the following sql statement:
select * from customers where (account_id = 13 and user_id = null) or (user_id in (1,3,4))
Sorry if I've completely missed the point here but I'm not 100% sure of what you want to do. Is AccessControl a relationship between User and Customer? If so looks like you just need to setup a many-to-many relationship.
class User
has_and_belongs_to_many :customers
# or this if you need to store meta data in the join table
has_many :customers
has_many :access_controls
has_many :accessible_customers, through => :access_controls
end

Resources