In my rails v4 app, users belong to a single group.
class User < ActiveRecord::Base
belongs_to :group
...
Each group can have many projects,
class Group < ActiveRecord::Base
has_many :projects
has_many :users
...
Each project can have many experiments, in a one to many relationship.
In my routes, I have:
resources :projects do
resources :experiments do
...
end
end
What I'd like to do is only allow users to access projects and experiments if the project has the same group_id as the user (i.e. if user enters a project id parameter in the projects#show route for a project outside of their group, it will not be displayed). Is there a clean way to implement this without having to do multiple checks in the view?
Take a look at building a custom constraint based on Group membership:
http://guides.rubyonrails.org/routing.html#advanced-constraints
Extremely simple example (obviously, you'll need to match your project design):
class GroupConstraint
def initialize
#project = Project.find(params[:id])
#user = current_user
end
def matches?(request)
#user.groups.include?(#project.group)
end
end
Then in your routes:
resources :projects, constraints: GroupConstraint.new do
resources :experiments do
...
end
end
This is authorization problem, so, as for me, it's better to define what users can and can not see using any auhtorization library, not using routes or something like that. Because you will, for example, definitely want to find out should you display link on given group in views, which groups is available for current user and so on.
Take a look on cancancan for example: https://github.com/CanCanCommunity/cancancan.
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.
Is it considered bad practice (or un-RESTful) to create nested routes for resources that otherwise have no association? For example I have:
resources :foos do
resources :bars
end
But I have no business logic elsewhere in my database or application that associates :foos with :bars.
The reason I want to do this: Many of my routes are created as resources nested under my :groups resource. I do that so that I can always grab a group_id param and always show a layout that matches the group the user is currently "in". I'm comfortable with this when the resource belongs_to the group:
/groups/1/comments/1
But when some other comment does not belong_to the group (group1) and I want to look at it through layout that is "branded" as group1, my impulse is to route it like this:
/groups/1/comments/2
Is this ok to do, maybe I'm overthinking this?
I maintain an app with similar requirements. I do something roughly along the lines of:
class User
has_and_belongs_to_many :groups
belongs_to :active_group, class_name 'Group'
def active_group
return super unless super.nil?
group = groups.first
update_columns(active_group_id: group.id)
group
end
end
The User class validates that they are assigned to one or more groups and the active_group method is overriden to provide a default if it is a first login. With this approach you will need to provide an action to set the active group so the user can switch groups (presumably this is a requirement).
This assumes that you have some kind of authentication in place to know the current user. If restricting access to groups is not a concern, you can forego the habtm relationship and substitute groups.first with Group.first.
If you don't have/want/need authentication, you could just drop a active_group_id in the session cookie. But I would definitely consider it bad practice to nest unrelated resources.
The Application Concept:
I'm building a Ruby on Rails [3.2] application at the moment, which consists of 2 very basic controllers - accounts for the user authentication, and messages which belong to the accounts.
# app/models/account.rb
class Account < ActiveRecord::Base
has_many :messages
end
# app/models/message.rb
class Message < ActiveRecord::Base
belongs_to :accounts
end
I then, of coarse, setup my routes.rb to allow users to view their messages nested under accounts:
# config/routes.rb
resources :accounts do
resources :messages
end
The Question:
I want users to access their accounts without using an ID parameter in the URL like this: example.com/accounts/, which works perfectly fine.
Whenever I try and go to: example.com/accounts/messages however, rails treats "messages" as a parameter for the accounts_controller! The only way I can access messages now is by going: /accounts/5236/messages - which is NOT what I want.
My question is, is there a way to block/mask rails from checking parameters on my accounts controller so that I can access my messages like the example above? I'm really puzzled on this one, so please share your thoughts and ideas!
Your defined routes
resources :accounts do
resources :messages
end
implies you can only have URL like this :
/accounts/
/accounts/:id
/accounts/:id/:action
/accounts/:id/messages/
/accounts/:id/messages/:id
/accounts/:id/messages/:id/:action
If you want specify the URL /accounts/messages/, you must specify it in the routes
resources :accounts, :collection => { :messages => :get } do
resource :messages
end
Just to add some closure to this question, I've decided to go with this solution in my routes.rb file:
get "/accounts/messages" => "messages#index", :as => :message
This works well, but the only downside is that it has to be manually added each time if you add controlers under the accounts namespace down the track. Oh well.
ForgetTheNorm also has a fantastic alternate solution below, so give that a shot if this doesn't work for you!
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
I'm using Ryan Bates' nifty authentication in my application for user signup and login. Each user has_many :widgets, but I'd like to allow users to browse other users' widgets. I'm thinking that a url scheme like /username/widgets/widget_id would make a lot of sense--it would keep all widget-related code in the same place (the widgets controller). However, I'm not sure how to use this style of URL in my app.
Right now my codebase is such that it permits logged-in users to browse only their own widgets, which live at /widgets/widget_id. What changes would I need to make to routes.rb, my models classes, and any place where links to a given widget are needed?
I've done Rails work before but am a newb when it comes to more complicated routing, etc, so I'd appreciate any feedback. Thanks for your consideration!
Look into nested routes. You could nest widgets inside users, like this:
map.resources :users do |users|
users.resources :widgets
end
Which would give you URLs like these:
/users/1/widgets # all of user 1's widgets
/users/1/widgets/1 # one of user 1's widgets
Check out the routing guide for more details.
The easiest would be to go with InheritedResources plugin which handles most of the legwork for you.
# routes:
map.resources :users do |user|
user.resources :widgets
end
class WidgetsController < InheritedResources::Base
# this will require :user_id to be passed on all requests
# #user will be set accordingly
# and widget will be searched in #user.widgets
belongs_to :user
end
# no changes required to the models