I have a users controller in my application, routed with:
map.resources :users
This has my user pages living at /users/1, and so forth.
I'd like my user pages to live at /users/blake, etc.
What's the right way to do this in rails, such that I can say link_to(#user) and the correct path is generated?
In model:
class User < ActiveRecord::Base
def to_param
login
end
end
In controller:
class UsersController < ApplicationController
def show
#user = User.find_by_login(params[:id])
#...
end
end
to_param in model is used by ActionPack to construct url for this object. And in controller you need to fetch your model by this field.
Related
Suppose we have the following setup in a ruby-on-rails (API) application:
class User < ActiveRecord::Base
has_many :posts
has_many :friends, class_name: User # Via a joins table....
end
class Post
belongs_to :user
end
When visiting /users/:id/posts, I want the logged-in user to only be able to view this data, if they are friends.
The standard implementation for this in Pundit is to use a policy scope:
class PostsController < ApplicationController
before_action :authenticate
def index
posts = policy_scope(Post.where(user_id: params[:user_id]))
render posts
end
end
class PostsPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
scope.where(user: user.friends)
end
end
end
This will prevent a user from seeing non-friends' posts. However, it produces an API response of 200 Success (with an empty response body), not 403 Forbidden - which would be preferable for the FrontEnd to receive, and display an appropriate error message.
Here's one solution that does not work:
class PostsController < ApplicationController
before_action :authenticate
def index
posts = policy_scope(Post.where(user_id: params[:user_id]))
authorize posts # <- !!!!
render posts
end
end
class PostsPolicy
def index?
record.all? { |post| user.friends_with?(post.user) }
end
end
Not only is this very inefficient, but if the user doesn't have any posts, then you'll always get a 200 Success response - which is still not ideal.
Similarly, it's not ideal to "return 403 if the response is empty" - because then you'd get error messages when viewing friends' posts, if they don't have any!
Here's a possible solution, but it feels wrong...
class PostsController < ApplicationController
before_action :authenticate
def index
user = user.find(params[:user_id])
authorize(user, :index_posts?) # <-- !!!!
posts = policy_scope(Post.where(user: user))
render posts
end
end
class UsersPolicy
def index_posts?
user.friends_with?(record)
end
end
(You could also use a more generic method name like UserPolicy#friends?, to the same affect.)
This works, but it feels like a mis-use of Pundit to be applying a UserPolicy method when authorising a Post resource!
Pundit does not allow passing additional arguments to policies. This has been a highly requested feature over the years. In particular, see this highly-relevant PR/discussion. In other words, what I'd like to be able to do is this:
class PostsController < ApplicationController
before_action :authenticate
def index
user = User.find(params[:user_id])
posts = policy_scope(Post.where(user: user))
authorize(posts, user) # <- !!!! (not valid in Pundit)
render posts
end
end
class PostsPolicy
def index?(other_user) # <- !!!! (not valid in Pundit)
user.friends_with?(other_user)
end
end
However, the feature was eventually conclusively rejected by the project maintainer in favour of using "name-spaced policies" and "form objects".
Hopefully this question is not too "opinionated", but what would you suggest? Is there a clean way to use the Pundit library whilst responding with appropriate 200 vs 403 appropriately?
Or, is there a good patch/fork/alternative I could use (preferably easy to migrate to, for a large project) that will better support my desired behaviour?
I have a Blog model which has different states. In order to keep a skinny controller and follow the convention of only having CRUD operations per controller, I followed DHH's namespacing controllers pattern and namespaced out the Blog controller.
Now I have a Blogs::NewDraft controller, a Blogs::AwaitingApproval controller, and a Blogs::Active controller.
The issue is with writing my policies to authorize the actions within these namespaced controllers. All the actions in all the namespaced controllers are all authorizing the same Blog model object. The issue is that I need each of the namespaced controllers to authorize in a matching namespaced policy (as opposed to all of the namespaced controllers authorizing within the same blog_policy.rb file.)
Basic Example: For a restful resource with a restful controller that is NOT namespaced you do it something like this:
#app/controllers/blogs_controller.rb
class BlogsController < ApplicationController
def index
authorize :blog
#blogs = Blog.all
end
def show
#blog = Blog.find(1)
authorize #blog
end
end
And now the matching Policy
#app/policies/blogs_policy.rb
class BlogPolicy < ApplicationPolicy
def index?
user.admin?
end
def show?
record.author == current_user
end
end
You do it like that when you don't namespace.
Current Code to Try to get Namespacing to work with Pundit: I am namespacing. I am still authorizing a Blog object, but I need to authorize the actions within each namespaced controller within a namespaced policy:
#app/controllers/blogs/new_drafts.rb
class Blogs::NewDraftsController < ApplicationController
def index
# doesn't work
authorize Blog::NewDrafts
#blogs = Blog.new_drafts
end
def show
#blog = Blog.find(1)
#doesn't work either
authorize #blog, Blog::NewDraft
end
end
So I want that namespaced controller to NOT route to app/policies/blog_policy.rb, but instead to app/policies/blogs/new_draft_policy.rb
#app/policies/blogs/new_draft_policy.rb
class Blogs::NewDraftPolicy < ApplicationPolicy
def index?
user.admin?
end
def show?
# the record is a blog from the Blog Model
record.author == current_user
end
end
Pundit Documentation and Usage
Don't know how to route to namespaced policy AND pass in the Blog record. However: below is how you do it when your namespaced policy is able to authorize only based on the current user's permissions/roles:
#app/controllers/blogs/new_drafts.rb
class Blogs::NewDraftsController < ApplicationController
def index
authorize [:blogs, :new_draft]
#blogs = Blog.new_drafts
end
end
#app/policies/blogs/new_draft_policy.rb
class Blogs::NewDraftPolicy < ApplicationPolicy
def index?
user.admin?
end
end
I have two models:
Student
Classroom
Both of them have an action that does the same exact thing: it shows a report of daily activity. That is:
/students/1
/classrooms/1
Grabs activity for the model in question and displays it on the page.
In an attempt to dry this up, I created a ReportsController which extracts all the common logic of building a report.
If I leave the routes like this:
/students/1/report
/classrooms/1/report
Then I can have the ReportsController#show action look for params for :student_id or :classroom_id to determine which model type it is dealing with (for purposes of querying the database and rendering the correct view).
But I would prefer the URLs to be cleaner, so I also changed my routes.rb file to pass the show action for these models to the reports#show controller action:
resources :students, :classrooms do
member do
get :show, to: 'reports#show'
end
end
This works, but I can no longer depend on params to identify which model to work with and which view to render.
Question: should I parse request.fullpath for the model? Or is there a better way to make a shared controller understand which model it is working with?
Routing both show methods to the same controller method for code reuse is somewhat like banging a nail in with a dumptruck.
Even if you can find the resource by looking at the request url you would start splitting the ResortsController into a bunch of ifs and switches even before you got off the ground.
One solution is to add the common action in a module:
module Reporting
extend ActiveSupport::Concern
def show
# the Student or Classroom should be available as #resource
render 'reports/show'
end
included do
before_action :find_resource, only: [:show]
end
private
def find_resource
model = self.try(:resource_class) || guess_resource_class
#resource = model.find(params[:id])
end
# This guesses the name of the resource based on the controller name.
def guess_resource_class
self.class.name[0..-11].singularize.constantize
end
end
class StudentController < ApplicationController
include Reporting
end
# Example where resource name cannot be deduced from controller
class PupilController < ApplicationController
include Reporting
private
def resource_class
Student
end
end
self.class.name[0..-11].singularize.constantize is basically how Rails uses convention over configuration to load a User automatically in your UsersController even without any code.
But the most important key to DRY controllers is to keep your controllers skinny. Most functionality can either be moved into the model layer or delegated out to service objects.
I would put the common logic in the Event Model:
#Event Model
class Event < ...
def self.your_event_method
#self here will be either student.events or classroom.events
#depending on which controller called it
end
end
class StudentsController < ...
...
def show
student = Student.find(params[:id])
student.events.your_event_method
end
end
class ClassroomsController < ...
...
def show
classroom = Classroom(params[:id])
classroom.events.your_event_method
end
end
I have some methods now under "profile" like user blocking, banning, moderation.
It feels these should belong under "user" and inside the user controller.
Is there a way to have a user_controller.rb when using devise with a user model?
Reason for this is to scope all user related methods under the user_controller instead of the profile_controller as it is now.
Yes. There is no problem with that. You can simply create users_controller.rb and interact with User model like:
class UsersController < ApplicationController
# do any stuff you need here
def block
#user = User.find(params[:id])
#user.block
end
def ban
#user = User.find(params[:id])
#user.ban
end
end
For sure, you have to create routes for this controller:
resources :users, only: [] do
member do
get :ban
get :block
end
end
Like that.
Suppose I have this association:
class User < ActiveRecord :: Base
has_one :car
end
class Car < ActiveRecord :: Base
belongs_to :user
end
routes:
resources :users do
resources :cars
end
Then what would be the code, in the 'new' action method in CarsController
class CarsController < ApplicationController
def new
#??? what's necessary to be put here,
# if I have request 'localhost:3000/users/1/cars/new'
end
...
end
Will Rails figure out everything automatically so I don't have to write any code in the 'new' method? Also, since the 'new' action will generate a 'form_for(#car)' form helper, how can I create this car object
Is this right?
class CarsController < ApplicationController
def new
#user = User.find(params[:user_id])
#car = #user.build_car({})
end
end
That looks just fine. Rails will not do any of this automatically. There are gems out there that can automate some of that if you like, but the jury is out on whether they're actually worth your time.
If you have no params to pass to the car, you can just run #user.build_car with no arguments, by the way. You'll also need to specifically say in the form_for helper that you're nesting the car under the user: form_for [#user, #car] Otherwise the form's destination will be /cars instead of /user/1/cars.
You're pretty close, Baboon. I would actually do:
class UsersController < ApplicationController
def new
#user = User.new
#car = #user.cars.build
end
end
But if you don't want to create the Car at the same time as the user, try:
class CarsController < ApplicationController
def new
#user = User.find(params[:user_id])
#car = #user.cars.build
end
end
I found the nested_form railscast extremely helpful when doing this sort of thing. In fact, I think you'll probably get a lot out of it (and using the nested_form gem).
railscasts_196-nested-model-form-part-1