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.
Related
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.
I have the following url /purchases/3/payments/new, which I get in the purchases controller by
link_to 'pay', purchase_path(purchase)+new_payment_path
Now in the payments controller I would need to have the purchase object, or its id at least from where it was invoked.
I tried using a param, but is there any other way ?
Thanks
Using params makes sense.
You should be able to get the purchase ID like this in the payments controller:
params[:purchase_id]
However, need to setup your routes in a specific way to do this:
resources :purchases do
resources :payments
end
Then you can create the link in the view like this:
link_to 'pay', new_purchase_payment_path(purchase)
Have a look at these docs too: http://guides.rubyonrails.org/routing.html#nested-resources
Routes
I immediately highlighted this as a problem:
purchase_path(purchase)+new_payment_path
This is really bad (configuration over convention) - you'll be much better using the actual path helper to load the resource you need (keeps it DRY & conventional)
--
Nested
As mentioned by Jon M, your solution will come from the use of nested resources:
#config/routes.rb
resources :purchases do
resources :payments # -> domain.com/purchases/:purchase_id/payments/new
end
This will allow you to use the route path as described by Jon.
--
Controller
in the payments controller I would need to have the purchase object,
or its id
By using nested resources, as described above, the route will hit your payments controller and have the following param available:
params[:purchase_id]
Note the naming of the param (only the nested resource is identified with params[:id]), as demonstrated in the docs: /magazines/:magazine_id/ads/:id
I would recommend using the following code in your controller:
#app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
def new
#purchase = Purchase.find params[:purchase_id]
end
end
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 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].
Is there a consensus best approach to implementing user roles when using RESTful resource routes?
Say I have the following resources:
User has_many Tickets
Event has_many Tickets
Ticket belongs_to Person, Event
And then further say I have two types of Users: customers and agents. Both will log into the system, but with different resource access and functionality based on their roles. For example:
Customers can access:
Event index, show
Ticket index (scoped by user), show, buy/create, return/delete
Person create, show, update
Agents can access:
Event index, show, create, update, delete
Ticket index, show, sell/create, update, refund/delete
Person index, show, create, update, delete
Which of the 4 general approaches below will be cleaner and more flexible?
Separate controllers within role folders and resources in namespaces, eg:
namespace "agent" do
resources :events, :tickets, :people
end
namespace "customer" do
resources :events, :tickets, :people
end
Separate controllers by role, eg:
AgentController
def sell_ticket, etc
CustomerController
def buy_ticket, etc
Shared controllers with separate actions where needed, eg:
TicketController
before_filter :customer_access, :only => :buy
before_filter :agent_access, :except => :buy
def buy #accessed by customer to create ticket
def sell #accessed by agent to create ticket
Shared actions with conditional statements, eg:
TicketController
def create
if #role == :customer
#buy ticket
elsif #role == :customer
#sell ticket
end
end
I would suggest using a combination of the last two proposed implementations. They adhere to RESTful representation, they put authorization at the appropriate level (controllers), and it is a scalable implementation.
REST is, essentially, about accessing nouns with verbs. So you want Agents and Customers to perform actions (verbs) in relation to Tickets, Users, and Events (nouns). In order to accurately represent these nouns you should have a controller for each. Customers can then identify the resource they are looking for by the URL, http://example.com/events/22. From here you can use Rails' routing to represent context for various resources, ie http://example.com/events/22/tickets by doing something like:
resource :events do
resource :tickets
end
By adhering to a RESTful architecture, you are buying into the end to end principle. The paradigm for representing objects needs to be only responsible for that. It shouldn't try to authenticate. That isn't its job. Authorization should happen in the controllers. I would highly recommend looking into gems like CanCan or Declarative Authorization that set all of this up for you.
Finally, this model scalable. By keeping authorization separate from the representation of your resources you only have to use it if you need it. This keeps your application light, flexible, and simple.
While they both deal with creating Tickets, the Agent / Ticket selling vs. the Customer / Ticket purchasing seem different enough to me that they should be separated. Eventually they could further diverge since they are being used so differently from the beginning.
It is possible to have shared controller functionality either with modules or by inheriting from a common parent controller:
module TicketsControllersCommom
# common helper methods
end
class TicketsController < ApplicationController
include TicketsControllersCommom
# actions
end
class AgentTicketsController < ApplicationController
include TicketsControllersCommom
# actions
end
I might treat the agent parts as a sort of admin section, with the customer parts being the default:
/events/xx/tickets # collection
/events/xx/tickets/xx # member
# etc.
/events/xx/agent/tickets # collection
/events/xx/agent/tickets/xx # member
# etc.
Or, if you have a lot of admin-type stuff, like if the agents manage the events as well, you can namespace a whole section:
/agent/events/xx/tickets
/agent/events/xx/edit
# etc.
If you use the same model for customer and agent tickets, there should be no major difference between how they are handled in controller. So, create action will be always like this:
#ticket = Ticket.new(params[:ticket])
if #ticket.save
redirect_to #ticket
else
render :action => "new"
end
But your views can be simply customized:
<% if customer? %>
Customer area.
<% else %>
Agent area.
<% end %>