I'm relatively new to Rails and busy building an app with various access levels, for instance global_admin and company_admin. Now company_admin should only have access to a specific company and no others.
My routes:
resources :companies do
resources :groups do
resources :users
end
end
I created a helper to check access which contains the following:
if params[:company_id].present?
#company = Company.find(params[:company_id])
...
So if I call, for instance ^/companies/1/groups ^/companies/1/groups/1/users the query returns true and finds the company_id, but if I call ^/companies/1 or ^/companies/2 it returns false. Why is it not picking up the company_id if it is (or at least seems to be) present?
Thanks in advance!
When you're not accessing a nested resource, params[:company_id] becomes params[:id] instead.
Same thing with groups. If you access /companies/1/groups/1, params[:id] would give you the group's id, but if you access /companies/1/groups/1/users/1, params[:id] would give you the user's id instead, and the group's id would be in params[:group_id].
Related
I am building an app which has a resource setup like the following:
User
Team
Invite
Project
Invite
users have one team. teams have many projects. users can be invited to join at either the teams level (and have access to any projects owned by the teams) or invited at the project level (only giving the invitee access to the single project).
I am trying to set up Invites to dynamically find it's parent resource (i.e: Team or Project). As I understand it, the best way would be to look at the path. Currently the path looks something like:
/teams/:id/invites/
/teams/:id/projects/:id/invites
Is it possible to look one "nesting level" back from the current resource in the path to find the parent resource in a controller action (e.g: invites#new)?
Thanks!
Clarification
I want to be able to use the same invites code for both the teams and projects resources. When the invites#new action is called, it checks the path to see what resource has called it. if the path is /teams/:id/invites/, it will return team and I can then find by :id, if the path is /teams/:id/projects/:id/invites, it will return project and again, I can then find by :id.
Is this possible?
You should not be nesting more than one level deep in the first place.
Rule of thumb: resources should never be nested more than 1 level
deep. A collection may need to be scoped by its parent, but a specific
member can always be accessed directly by an id, and shouldn’t need
scoping (unless the id is not unique, for some reason).
- Jamis Buck
Your paths should look like:
/teams/:team_id/invites
/projects/:project_id/invites
This provides all the context needed! Adding more nesting just adds bloat and over-complication and makes a poor API.
To create a reusable controller for a nested polymorphic resource you can use a routing concern:
concerns :inviteable do
resources :invites, shallow: true
end
resources :teams, concerns: :inviteable
resources :projects, concerns: :inviteable
You can then set up a controller for invites which checks what parent param is present:
class InvitesController < ApplicationController
before_action :set_parent, only: [:new, :create, :index]
# GET /teams/:team_id/invites/new
# GET /projects/:team_id/invites/new
def new
#invite = #parent.invites.new
end
# GET /teams/:team_id/invites
# GET /projects/:team_id/invites
def index
#invites = #parent.invites
end
# POST /teams/:team_id/invites
# POST /projects/:team_id/invites
def create
#invite = #parent.invites.new(invite_params)
# ...
end
# ...
private
def parent_class
if params[:team_id]
Team
elsif params[:project_id]
Project
end
end
def parent_param
params[ parent_class.model_name.singular_route_key + "_id" ]
end
def set_parent
#parent = parent_class.find(parent_param)
end
end
When the route is:
/teams/:team_id/invites/new //note that it should be team_id, not :id,
or
/teams/:team_id/projects/:project_id/invites/new
you could always to check the nesting by these params. If
params[:project_id].present?
then you are under /teams/:team_id/projects/:project_id/invites route, the invitable_type should be Project. Otherwise, it should be /teams/:team_id/invites/, and the invitable_type should be Team.
I've been wondering if there is any better way to handle one-to-one relations with Rails.
I have a User model, which has_one Subscription. When the user is logged in, I get the subscription by retreiving current_user.subscription, so that finding subscriptions by id in URL is unncessary.
Right now, when user wants to update his subscription, he gets the url:
/subscription/3/?plan_id=2
But the subscription 3 is unncessary, and the other thing is I don't want to be showing the number of subscriptions (ids) to the user.
Whats would be a better solution for this? Would you guys bother with that?
Thanks
You could use resource :subscriptions (not resources). As described here (link)
the difference is, that resource doesn't create an index route (only if you say explicitly that you want one) and doesn't need IDs in the url
Quote from the linked answer:
At a high level, the intent of resource is to declare that only one of
these resources will ever exist. For example:
resource :profile, :only => [:edit, :update] As a user, I should only
be able to update my own profile. I should never be able to edit other
users' profiles, so there's no need for a URL scheme like
/users/1/profile/edit. Instead, I use /profile/edit, and the
controller knows to use the current user's ID rather than the ID
passed in the URL (since there is none).
That's why you don't get an index action with resource: there's only one resource, so > there's no sense in "listing" them.
I guess You have something like this defined in Your routes
resources :subscriptions
What You want is a collection level custom action which is defined like this:
resources :subscriptions do
get 'new_plan', on: :collection
end
Now You will need to add an action in You controller of the same name. And the link to get there will be: /subscription/new_plan?plan_id=2
I have two models:
class GarageOwner < ActiveRecord::Base
has_many :garages, dependent: :destroy
end
class Garage < ActiveRecord::Base
belongs_to :garage_owner
end
A Garage should never exist without a garage owner. So in the new action of the GaragesController I need the corresponding garage owner. I do not want to use nested routes so I do not have the garage owners id as a parameter. But how do I get him then?
Update for some clarification
Garages are created by a third model (Admin). So I can not access the garage owner through the current user.
I build my routes using resources:
garage_owners GET /garage_owners(.:format) garage_owners#index
POST /garage_owners(.:format) garage_owners#create
new_garage_owner GET /garage_owners/new(.:format) garage_owners#new
edit_garage_owner GET /garage_owners/:id/edit(.:format) garage_owners#edit
garage_owner GET /garage_owners/:id(.:format) garage_owners#show
PUT /garage_owners/:id(.:format) garage_owners#update
DELETE /garage_owners/:id(.:format) garage_owners#destroy
garages GET /garages(.:format) garages#index
POST /garages(.:format) garages#create
new_garage GET /garages/new(.:format) garages#new
edit_garage GET /garages/:id/edit(.:format) garages#edit
garage GET /garages/:id(.:format) garages#show
PUT /garages/:id(.:format) garages#update
DELETE /garages/:id(.:format) garages#destroy
The solution to not using nested routes is to insert the garage_owner_id as a hidden field in your new garage form. But, you've given no indication in your question of how the new garage form is meant to know about which garage_owner it should associate with so I can't give you a specific example.
Perhaps I'm not getting your question, but I think you'd have to either pick up the garage owner from your session (e.g. logged in user), or something derived from a value in your session or as a value submitted with the form used for the new operation, in which case it would be a parameter.
You could approach this a couple different ways:
If the GarageOwer requires a login, you could grab the ID of the GarageOwer user from the cookie stored at login.
Create a custom route match "/Garages/new/:owner_id" => "garages#new", there after in your controller access the owner's id via params[:owner_id].
Add owner_id as a hidden attributed to the form on the "garages/new" page.
I found an appropriate solution which is still restful I think. I just created multiple routes for garages:
resources :garage_owners do
resources :garages, except: :index
end
resources :garages, only: [:index, :show]
Normal users should not access the other actions.
There is Permission Model, which doesn't have related db table, and is used to authorization.
With before_filter authorize method creates new permission object, depending on user and optional test_id (another model).
The idea is that it checks if test belongs to user, it allows this user delete this test, if no, it cancels transaction.
So my initialize method:
class Permission
def initialize(user, *test_id)
#user = user
if !test_id.empty?
test_id.each do |t|
test = Test.find_by_id(t) #finding real test record
self.instance_variable_set(:"#test_#{t}", test) #should set #test_{id}
#an instance of this new permission refering to actual test record
end
end
allow :users, [:new,:create]
allow :root,[]
allow :session, [:create, :destroy]
allow :tests, [:new, :create]
if !test_id.empty?
for i in 0...test_id.length
t = self.instance_variable_get(:"#test_#{i}") #getting this test record.
if t.user_id == #user.id
allow :tests, [:edit,:lock,:unlock, :destroy ]
end
end
end
end
The problem is that what rails gets from instance_variable_get is nil. So or I set up instance #test_{id} wrong, or get it.
Thanks in advance
The instance variables you set in the top each are of the form #test_{record_id}. The instance variables you get in the for loop are of the form #test_{loop_index}. Just use the same loop as at the top.
test_id.each do |t|
trec = self.instance_variable_get(:"#test_#{t}") #getting this test record.
if trec.user_id == #user.id
allow :tests, [:edit,:lock,:unlock, :destroy ]
end
end
You are using the instance variables as a database with the primary key based on the variable name itself. Needless (I think?) to say, this is not a good practice. You can use instance_variable_set to set dynamic instance variable names, but in general, I feel this makes your classes unpredictable, as it is more difficult to keep track of how the behavior is implemented with respect to the data.
If you need to cache a number of objects in your instance, you can use a data structure like an array or a hash, and set that to its own instance variable.
Ok, guys, I've just found out, that I overcomplicated things hardly.
I really don't need to set or get instance variables in my case, so
if !test_id.empty?
test_id.each do |t|
test=Test.find_by_id(t)
if test.user_id==user.id
allow :tests, [:edit,:lock,:unlock, :destroy ]
end
end
end
works fine
In my app I have a User model which defines a history method that returns a list of Activity objects, showing the last N actions the user has carried out. The UserController#history method wires this with a view.
The code looks as follows:
class UserController < ApplicationController
def history
user = User.find(params[:id])
#history = user.history(20)
end
end
class User < ActiveRecord::Base
has_many :activities
def history(limit)
...
end
end
Naturally, I also added this line to my routes.rb file:
match '/user/:id/:action', :controller => 'user'
so now when I go to localhost:3000/user/8/history I see the history of user 8. Everything works fine.
Being a Rails NOOB I was wondering whether there is some canned solution for this situation which can simplify the code. I mean, if /user/8 is the RESTful way for accessing the page of User 8, is it possible to tell Rails that /user/8/history should show the data returned by invoking history() on User 8?
First of all the convention to name controllers is in the plural form unless it is only for a single resource, for example a session.
About the routes I believe you used the resources "helper" in your routes, what you can do is specify that the resource routes to users also has a member action to get the history like this
resources :users do
member do
get :history
end
end
I think there is no cleaner way to do this
You can check it here http://guides.rubyonrails.org/routing.html#adding-more-restful-actions
As far as the rails standards are concerned, it is the correct way to show the history in your case. In rails controllers are suppose to be middle-ware of views and model, so defining an action history seems good to me.
And you can specify the routes in better way as:
resources :user do
get 'history', :on => :member #it will generate users/:id/history as url.
end